diff -Nru thunderbird-102.9.0+build1/accessible/base/ARIAMap.cpp thunderbird-102.10.0+build2/accessible/base/ARIAMap.cpp --- thunderbird-102.9.0+build1/accessible/base/ARIAMap.cpp 2023-03-11 11:24:06.000000000 +0000 +++ thunderbird-102.10.0+build2/accessible/base/ARIAMap.cpp 2023-04-11 06:10:53.000000000 +0000 @@ -1446,10 +1446,22 @@ } else if (aRoleMapEntry == &sLandmarkRoleMap) { return LANDMARK_ROLE_MAP_ENTRY_INDEX; } else { - return aRoleMapEntry - sWAIRoleMaps; + uint8_t index = aRoleMapEntry - sWAIRoleMaps; + MOZ_ASSERT(aria::IsRoleMapIndexValid(index)); + return index; } } +bool aria::IsRoleMapIndexValid(uint8_t aRoleMapIndex) { + switch (aRoleMapIndex) { + case NO_ROLE_MAP_ENTRY_INDEX: + case EMPTY_ROLE_MAP_ENTRY_INDEX: + case LANDMARK_ROLE_MAP_ENTRY_INDEX: + return true; + } + return aRoleMapIndex < ArrayLength(sWAIRoleMaps); +} + uint64_t aria::UniversalStatesFor(mozilla::dom::Element* aElement) { uint64_t state = 0; uint32_t index = 0; diff -Nru thunderbird-102.9.0+build1/accessible/base/ARIAMap.h thunderbird-102.10.0+build2/accessible/base/ARIAMap.h --- thunderbird-102.9.0+build1/accessible/base/ARIAMap.h 2023-03-11 11:24:06.000000000 +0000 +++ thunderbird-102.10.0+build2/accessible/base/ARIAMap.h 2023-04-11 06:10:53.000000000 +0000 @@ -259,6 +259,11 @@ uint8_t GetIndexFromRoleMap(const nsRoleMapEntry* aRoleMap); /** + * Determine whether a role map entry index is valid. + */ +bool IsRoleMapIndexValid(uint8_t aRoleMapIndex); + +/** * Return accessible state from ARIA universal states applied to the given * element. */ diff -Nru thunderbird-102.9.0+build1/accessible/ipc/DocAccessibleParent.cpp thunderbird-102.10.0+build2/accessible/ipc/DocAccessibleParent.cpp --- thunderbird-102.9.0+build1/accessible/ipc/DocAccessibleParent.cpp 2023-03-11 11:24:06.000000000 +0000 +++ thunderbird-102.10.0+build2/accessible/ipc/DocAccessibleParent.cpp 2023-04-11 06:10:53.000000000 +0000 @@ -4,6 +4,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "ARIAMap.h" #include "CachedTableAccessible.h" #include "DocAccessibleParent.h" #include "mozilla/a11y/Platform.h" @@ -145,6 +146,10 @@ aParent->AddChildAt(aIdxInParent, newProxy); newProxy->SetParent(aParent); } else { + if (!aria::IsRoleMapIndexValid(newChild.RoleMapEntryIndex())) { + MOZ_ASSERT_UNREACHABLE("Invalid role map entry index"); + return 0; + } newProxy = new RemoteAccessible( newChild.ID(), aParent, this, newChild.Role(), newChild.Type(), newChild.GenericTypes(), newChild.RoleMapEntryIndex()); @@ -246,8 +251,18 @@ return IPC_OK(); } +#ifdef XP_WIN + WeakPtr parent = root->RemoteParent(); +#else RemoteAccessible* parent = root->RemoteParent(); +#endif ProxyShowHideEvent(root, parent, false, aFromUser); +#ifdef XP_WIN + if (!parent) { + MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup()); + return IPC_FAIL(this, "Parent removed while removing child"); + } +#endif RefPtr event = nullptr; if (nsCoreUtils::AccEventObserversExist()) { @@ -726,7 +741,12 @@ return IPC_FAIL(this, "binding to nonexistant proxy!"); } +#ifdef XP_WIN + WeakPtr outerDoc = e->mProxy; +#else RemoteAccessible* outerDoc = e->mProxy; +#endif + MOZ_ASSERT(outerDoc); // OuterDocAccessibles are expected to only have a document as a child. @@ -781,6 +801,9 @@ # endif // defined(MOZ_SANDBOX) } } + if (!outerDoc) { + return IPC_FAIL(this, "OuterDoc removed while adding child doc"); + } // Send a COM proxy for the embedder OuterDocAccessible to the embedded // document process. This will be returned as the parent of the // embedded document. @@ -818,6 +841,9 @@ # endif // defined(MOZ_SANDBOX) } } + if (!outerDoc) { + return IPC_FAIL(this, "OuterDoc removed while adding child doc"); + } } if (nsWinUtils::IsWindowEmulationStarted()) { aChildDoc->SetEmulatedWindowHandle(mEmulatedWindowHandle); diff -Nru thunderbird-102.9.0+build1/accessible/ipc/RemoteAccessibleBase.h thunderbird-102.10.0+build2/accessible/ipc/RemoteAccessibleBase.h --- thunderbird-102.9.0+build1/accessible/ipc/RemoteAccessibleBase.h 2023-03-11 11:24:06.000000000 +0000 +++ thunderbird-102.10.0+build2/accessible/ipc/RemoteAccessibleBase.h 2023-04-11 06:10:53.000000000 +0000 @@ -11,6 +11,7 @@ #include "mozilla/a11y/CacheConstants.h" #include "mozilla/a11y/HyperTextAccessibleBase.h" #include "mozilla/a11y/Role.h" +#include "mozilla/WeakPtr.h" #include "AccAttributes.h" #include "nsIAccessibleText.h" #include "nsIAccessibleTypes.h" @@ -27,7 +28,13 @@ enum class RelationType; template +#ifdef XP_WIN +class RemoteAccessibleBase : public Accessible, + public HyperTextAccessibleBase, + public SupportsWeakPtr { +#else class RemoteAccessibleBase : public Accessible, public HyperTextAccessibleBase { +#endif public: virtual ~RemoteAccessibleBase() { MOZ_ASSERT(!mWrapper); } diff -Nru thunderbird-102.9.0+build1/accessible/ipc/win/RemoteAccessible.cpp thunderbird-102.10.0+build2/accessible/ipc/win/RemoteAccessible.cpp --- thunderbird-102.9.0+build1/accessible/ipc/win/RemoteAccessible.cpp 2023-03-11 11:24:06.000000000 +0000 +++ thunderbird-102.10.0+build2/accessible/ipc/win/RemoteAccessible.cpp 2023-04-11 06:10:53.000000000 +0000 @@ -38,7 +38,7 @@ // methods here in RemoteAccessible, causing infinite recursion. MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup()); if (!mCOMProxy && mSafeToRecurse) { - RemoteAccessible* thisPtr = const_cast(this); + WeakPtr thisPtr = const_cast(this); // See if we can lazily obtain a COM proxy MsaaAccessible* msaa = MsaaAccessible::GetFrom(thisPtr); bool isDefunct = false; @@ -48,7 +48,12 @@ VARIANT realId = {{{VT_I4}}}; realId.ulVal = msaa->GetExistingID(); MOZ_DIAGNOSTIC_ASSERT(realId.ulVal != CHILDID_SELF); - thisPtr->mCOMProxy = msaa->GetIAccessibleFor(realId, &isDefunct); + RefPtr proxy = msaa->GetIAccessibleFor(realId, &isDefunct); + if (!thisPtr) { + *aOutAccessible = nullptr; + return false; + } + thisPtr->mCOMProxy = proxy; } RefPtr addRefed = mCOMProxy; diff -Nru thunderbird-102.9.0+build1/accessible/windows/msaa/MsaaAccessible.cpp thunderbird-102.10.0+build2/accessible/windows/msaa/MsaaAccessible.cpp --- thunderbird-102.9.0+build1/accessible/windows/msaa/MsaaAccessible.cpp 2023-03-11 11:24:06.000000000 +0000 +++ thunderbird-102.10.0+build2/accessible/windows/msaa/MsaaAccessible.cpp 2023-04-11 06:10:53.000000000 +0000 @@ -665,10 +665,16 @@ dom::BrowserParent* aBrowser, Callback aCallback) { // We can't use BrowserBridgeParent::VisitAllDescendants because it doesn't // provide a way to stop the search. - const auto& bridges = aBrowser->ManagedPBrowserBridgeParent(); - return std::all_of(bridges.cbegin(), bridges.cend(), [&](const auto& key) { - auto* bridge = static_cast(key); - dom::BrowserParent* childBrowser = bridge->GetBrowserParent(); + const auto& rawBridges = aBrowser->ManagedPBrowserBridgeParent(); + nsTArray> bridges(rawBridges.Count()); + for (const auto bridge : rawBridges) { + bridges.AppendElement(static_cast(bridge)); + } + return std::all_of(bridges.cbegin(), bridges.cend(), [&](const auto& bridge) { + RefPtr childBrowser = bridge->GetBrowserParent(); + if (!childBrowser) { + return true; + } DocAccessibleParent* childDocAcc = childBrowser->GetTopLevelDocAccessible(); if (!childDocAcc || childDocAcc->IsShutdown()) { return true; diff -Nru thunderbird-102.9.0+build1/browser/components/attribution/AttributionCode.jsm thunderbird-102.10.0+build2/browser/components/attribution/AttributionCode.jsm --- thunderbird-102.9.0+build1/browser/components/attribution/AttributionCode.jsm 2023-03-11 11:24:07.000000000 +0000 +++ thunderbird-102.10.0+build2/browser/components/attribution/AttributionCode.jsm 2023-04-11 06:10:53.000000000 +0000 @@ -66,6 +66,7 @@ "ua", "dltoken", "msstoresignedin", + "dlsource", ]; let gCachedAttrData = null; diff -Nru thunderbird-102.9.0+build1/browser/components/attribution/test/xpcshell/head.js thunderbird-102.10.0+build2/browser/components/attribution/test/xpcshell/head.js --- thunderbird-102.9.0+build1/browser/components/attribution/test/xpcshell/head.js 2023-03-11 11:24:06.000000000 +0000 +++ thunderbird-102.10.0+build2/browser/components/attribution/test/xpcshell/head.js 2023-04-11 06:10:53.000000000 +0000 @@ -64,6 +64,12 @@ code: "dltoken%3Dc18f86a3-f228-4d98-91bb-f90135c0aa9c", parsed: { dltoken: "c18f86a3-f228-4d98-91bb-f90135c0aa9c" }, }, + { + code: "dlsource%3Dsome-dl-source", + parsed: { + dlsource: "some-dl-source", + }, + }, ]; let invalidAttrCodes = [ diff -Nru thunderbird-102.9.0+build1/browser/components/enterprisepolicies/Policies.jsm thunderbird-102.10.0+build2/browser/components/enterprisepolicies/Policies.jsm --- thunderbird-102.9.0+build1/browser/components/enterprisepolicies/Policies.jsm 2023-03-11 11:24:07.000000000 +0000 +++ thunderbird-102.10.0+build2/browser/components/enterprisepolicies/Policies.jsm 2023-04-11 06:10:54.000000000 +0000 @@ -31,6 +31,7 @@ BookmarksPolicies: "resource:///modules/policies/BookmarksPolicies.jsm", CustomizableUI: "resource:///modules/CustomizableUI.jsm", FileUtils: "resource://gre/modules/FileUtils.jsm", + PdfJsDefaultPreferences: "resource://pdf.js/PdfJsDefaultPreferences.jsm", ProxyPolicies: "resource:///modules/policies/ProxyPolicies.jsm", WebsiteFilter: "resource:///modules/policies/WebsiteFilter.jsm", }); @@ -1669,7 +1670,7 @@ Preferences: { onBeforeAddons(manager, param) { - const allowedPrefixes = [ + let allowedPrefixes = [ "accessibility.", "app.update.", "browser.", @@ -1695,6 +1696,9 @@ "ui.", "widget.", ]; + if (!AppConstants.MOZ_REQUIRE_SIGNING) { + allowedPrefixes.push("xpinstall.signatures.required"); + } const allowedSecurityPrefs = [ "security.block_fileuri_script_with_wrong_mime", "security.default_personal_cert", @@ -1770,7 +1774,22 @@ // automatically converting these values to booleans. // Since we allow arbitrary prefs now, we have to do // something different. See bug 1666836. - if ( + // Even uglier, because pdfjs prefs are set async, we need + // to get their type from PdfJsDefaultPreferences. + if (preference.startsWith("pdfjs.")) { + let preferenceTail = preference.replace("pdfjs.", ""); + if ( + preferenceTail in PdfJsDefaultPreferences && + typeof PdfJsDefaultPreferences[preferenceTail] == "number" + ) { + prefBranch.setIntPref(preference, param[preference].Value); + } else { + prefBranch.setBoolPref( + preference, + !!param[preference].Value + ); + } + } else if ( prefBranch.getPrefType(preference) == prefBranch.PREF_INT || ![0, 1].includes(param[preference].Value) ) { diff -Nru thunderbird-102.9.0+build1/browser/components/enterprisepolicies/tests/xpcshell/head.js thunderbird-102.10.0+build2/browser/components/enterprisepolicies/tests/xpcshell/head.js --- thunderbird-102.9.0+build1/browser/components/enterprisepolicies/tests/xpcshell/head.js 2023-03-11 11:24:07.000000000 +0000 +++ thunderbird-102.10.0+build2/browser/components/enterprisepolicies/tests/xpcshell/head.js 2023-04-11 06:10:53.000000000 +0000 @@ -51,7 +51,7 @@ true, `Pref ${prefName} is correctly locked` ); - equal( + strictEqual( Preferences.get(prefName), prefValue, `Pref ${prefName} has the correct value` @@ -64,7 +64,7 @@ false, `Pref ${prefName} is correctly unlocked` ); - equal( + strictEqual( Preferences.get(prefName), prefValue, `Pref ${prefName} has the correct value` @@ -72,7 +72,7 @@ } function checkUserPref(prefName, prefValue) { - equal( + strictEqual( Preferences.get(prefName), prefValue, `Pref ${prefName} has the correct value` diff -Nru thunderbird-102.9.0+build1/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js thunderbird-102.10.0+build2/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js --- thunderbird-102.9.0+build1/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js 2023-03-11 11:24:06.000000000 +0000 +++ thunderbird-102.10.0+build2/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js 2023-04-11 06:10:53.000000000 +0000 @@ -967,6 +967,26 @@ "print.prefer_system_dialog": true, }, }, + + // Bug 1820195 + { + policies: { + Preferences: { + "pdfjs.annotationMode": { + Value: 1, + Status: "default", + }, + "pdfjs.sidebarViewOnLoad": { + Value: 0, + Status: "default", + }, + }, + }, + unlockedPrefs: { + "pdfjs.annotationMode": 1, + "pdfjs.sidebarViewOnLoad": 0, + }, + }, ]; add_task(async function test_policy_simple_prefs() { diff -Nru thunderbird-102.9.0+build1/browser/config/version_display.txt thunderbird-102.10.0+build2/browser/config/version_display.txt --- thunderbird-102.9.0+build1/browser/config/version_display.txt 2023-03-11 11:24:07.000000000 +0000 +++ thunderbird-102.10.0+build2/browser/config/version_display.txt 2023-04-11 06:11:36.000000000 +0000 @@ -1 +1 @@ -102.9.0esr +102.10.0esr diff -Nru thunderbird-102.9.0+build1/browser/config/version.txt thunderbird-102.10.0+build2/browser/config/version.txt --- thunderbird-102.9.0+build1/browser/config/version.txt 2023-03-11 11:24:07.000000000 +0000 +++ thunderbird-102.10.0+build2/browser/config/version.txt 2023-04-11 06:11:36.000000000 +0000 @@ -1 +1 @@ -102.9.0 +102.10.0 diff -Nru thunderbird-102.9.0+build1/build/debian-packages/mercurial-timeout.diff thunderbird-102.10.0+build2/build/debian-packages/mercurial-timeout.diff --- thunderbird-102.9.0+build1/build/debian-packages/mercurial-timeout.diff 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/build/debian-packages/mercurial-timeout.diff 2023-04-11 06:10:54.000000000 +0000 @@ -0,0 +1,38 @@ +# HG changeset patch +# User Julien Cristau +# Date 1679408823 -3600 +# Tue Mar 21 15:27:03 2023 +0100 +# Branch stable +# Node ID 3a889388b8f5e4fc884fd7eb6c5daf82056627e2 +# Parent 411dc27fd9fd076d6a031a08fcaace659afe2fe3 +url: don't ignore timeout for https connections + +For http, we use the stdlib's HTTPConnection.connect which passes the +timeout down to socket.create_connection; for https, we override the +connect method but weren't handling the timeout, so connections could +hang for hours even with http.timeout set to low values. + +diff --git a/mercurial/url.py b/mercurial/url.py +--- a/mercurial/url.py ++++ b/mercurial/url.py +@@ -404,17 +404,19 @@ if has_https: + *args, + **kwargs + ): + keepalive.HTTPConnection.__init__(self, host, port, *args, **kwargs) + self.key_file = key_file + self.cert_file = cert_file + + def connect(self): +- self.sock = socket.create_connection((self.host, self.port)) ++ self.sock = socket.create_connection( ++ (self.host, self.port), self.timeout ++ ) + + host = self.host + if self.realhostport: # use CONNECT proxy + _generic_proxytunnel(self) + host = self.realhostport.rsplit(b':', 1)[0] + self.sock = sslutil.wrapsocket( + self.sock, + self.key_file, diff -Nru thunderbird-102.9.0+build1/BUILDID thunderbird-102.10.0+build2/BUILDID --- thunderbird-102.9.0+build1/BUILDID 2023-03-11 11:33:26.000000000 +0000 +++ thunderbird-102.10.0+build2/BUILDID 2023-04-11 06:18:48.000000000 +0000 @@ -1 +1 @@ -20230310165821 \ No newline at end of file +20230407145224 \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/CLOBBER thunderbird-102.10.0+build2/CLOBBER --- thunderbird-102.9.0+build1/CLOBBER 2023-03-11 11:24:06.000000000 +0000 +++ thunderbird-102.10.0+build2/CLOBBER 2023-04-11 06:11:36.000000000 +0000 @@ -22,4 +22,4 @@ # changes to stick? As of bug 928195, this shouldn't be necessary! Please # don't change CLOBBER for WebIDL changes any more. -Merge day clobber 2023-02-14 \ No newline at end of file +Merge day clobber 2023-03-13 \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/calendar/timezones/zones.json thunderbird-102.10.0+build2/comm/calendar/timezones/zones.json --- thunderbird-102.9.0+build1/comm/calendar/timezones/zones.json 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/calendar/timezones/zones.json 2023-04-11 06:11:52.000000000 +0000 @@ -1596,8 +1596,7 @@ }, "America/Mexico_City": { "ics": [ - "BEGIN:DAYLIGHT\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0500\r\nTZNAME:CDT\r\nDTSTART:19700405T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU\r\nEND:DAYLIGHT", - "BEGIN:STANDARD\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19701025T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD" + "BEGIN:STANDARD\r\nTZOFFSETFROM:-0600\r\nTZOFFSETTO:-0600\r\nTZNAME:CST\r\nDTSTART:19700101T000000\r\nEND:STANDARD" ], "latitude": "+0192400", "longitude": "-0990900" diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/browser-request/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/browser-request/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/browser-request/index.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/browser-request/index.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,496 +0,0 @@ -// Browser Request -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// UMD HEADER START -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define([], factory); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like enviroments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - root.returnExports = factory(); - } -}(this, function () { -// UMD HEADER END - -var XHR = XMLHttpRequest -if (!XHR) throw new Error('missing XMLHttpRequest') -request.log = { - 'trace': noop, 'debug': noop, 'info': noop, 'warn': noop, 'error': noop -} - -var DEFAULT_TIMEOUT = 3 * 60 * 1000 // 3 minutes - -// -// request -// - -function request(options, callback) { - // The entry-point to the API: prep the options object and pass the real work to run_xhr. - if(typeof callback !== 'function') - throw new Error('Bad callback given: ' + callback) - - if(!options) - throw new Error('No options given') - - var options_onResponse = options.onResponse; // Save this for later. - - if(typeof options === 'string') - options = {'uri':options}; - else - options = JSON.parse(JSON.stringify(options)); // Use a duplicate for mutating. - - options.onResponse = options_onResponse // And put it back. - - if (options.verbose) request.log = getLogger(); - - if(options.url) { - options.uri = options.url; - delete options.url; - } - - if(!options.uri && options.uri !== "") - throw new Error("options.uri is a required argument"); - - if(typeof options.uri != "string") - throw new Error("options.uri must be a string"); - - var unsupported_options = ['proxy', '_redirectsFollowed', 'maxRedirects', 'followRedirect'] - for (var i = 0; i < unsupported_options.length; i++) - if(options[ unsupported_options[i] ]) - throw new Error("options." + unsupported_options[i] + " is not supported") - - options.callback = callback - options.method = options.method || 'GET'; - options.headers = options.headers || {}; - options.body = options.body || null - options.timeout = options.timeout || request.DEFAULT_TIMEOUT - - if(options.headers.host) - throw new Error("Options.headers.host is not supported"); - - if(options.json) { - options.headers.accept = options.headers.accept || 'application/json' - if(options.method !== 'GET') - options.headers['content-type'] = 'application/json' - - if(typeof options.json !== 'boolean') - options.body = JSON.stringify(options.json) - else if(typeof options.body !== 'string') - options.body = JSON.stringify(options.body) - } - - //BEGIN QS Hack - var serialize = function(obj) { - var str = []; - for(var p in obj) - if (obj.hasOwnProperty(p)) { - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); - } - return str.join("&"); - } - - if(options.qs){ - var qs = (typeof options.qs == 'string')? options.qs : serialize(options.qs); - if(options.uri.indexOf('?') !== -1){ //no get params - options.uri = options.uri+'&'+qs; - }else{ //existing get params - options.uri = options.uri+'?'+qs; - } - } - //END QS Hack - - //BEGIN FORM Hack - var multipart = function(obj) { - //todo: support file type (useful?) - var result = {}; - result.boundry = '-------------------------------'+Math.floor(Math.random()*1000000000); - var lines = []; - for(var p in obj){ - if (obj.hasOwnProperty(p)) { - lines.push( - '--'+result.boundry+"\n"+ - 'Content-Disposition: form-data; name="'+p+'"'+"\n"+ - "\n"+ - obj[p]+"\n" - ); - } - } - lines.push( '--'+result.boundry+'--' ); - result.body = lines.join(''); - result.length = result.body.length; - result.type = 'multipart/form-data; boundary='+result.boundry; - return result; - } - - if(options.form){ - if(typeof options.form == 'string') throw('form name unsupported'); - if(options.method === 'POST'){ - var encoding = (options.encoding || 'application/x-www-form-urlencoded').toLowerCase(); - options.headers['content-type'] = encoding; - switch(encoding){ - case 'application/x-www-form-urlencoded': - options.body = serialize(options.form).replace(/%20/g, "+"); - break; - case 'multipart/form-data': - var multi = multipart(options.form); - //options.headers['content-length'] = multi.length; - options.body = multi.body; - options.headers['content-type'] = multi.type; - break; - default : throw new Error('unsupported encoding:'+encoding); - } - } - } - //END FORM Hack - - // If onResponse is boolean true, call back immediately when the response is known, - // not when the full request is complete. - options.onResponse = options.onResponse || noop - if(options.onResponse === true) { - options.onResponse = callback - options.callback = noop - } - - // XXX Browsers do not like this. - //if(options.body) - // options.headers['content-length'] = options.body.length; - - // HTTP basic authentication - if(!options.headers.authorization && options.auth) - options.headers.authorization = 'Basic ' + b64_enc(options.auth.username + ':' + options.auth.password); - - return run_xhr(options) -} - -var req_seq = 0 -function run_xhr(options) { - var xhr = new XHR - , timed_out = false - , is_cors = is_crossDomain(options.uri) - , supports_cors = ('withCredentials' in xhr) - - req_seq += 1 - xhr.seq_id = req_seq - xhr.id = req_seq + ': ' + options.method + ' ' + options.uri - xhr._id = xhr.id // I know I will type "_id" from habit all the time. - - if(is_cors && !supports_cors) { - var cors_err = new Error('Browser does not support cross-origin request: ' + options.uri) - cors_err.cors = 'unsupported' - return options.callback(cors_err, xhr) - } - - xhr.timeoutTimer = setTimeout(too_late, options.timeout) - function too_late() { - timed_out = true - var er = new Error('ETIMEDOUT') - er.code = 'ETIMEDOUT' - er.duration = options.timeout - - request.log.error('Timeout', { 'id':xhr._id, 'milliseconds':options.timeout }) - return options.callback(er, xhr) - } - - // Some states can be skipped over, so remember what is still incomplete. - var did = {'response':false, 'loading':false, 'end':false} - - xhr.onreadystatechange = on_state_change - xhr.open(options.method, options.uri, true) // asynchronous - if(is_cors) - xhr.withCredentials = !! options.withCredentials - xhr.send(options.body) - return xhr - - function on_state_change(event) { - if(timed_out) - return request.log.debug('Ignoring timed out state change', {'state':xhr.readyState, 'id':xhr.id}) - - request.log.debug('State change', {'state':xhr.readyState, 'id':xhr.id, 'timed_out':timed_out}) - - if(xhr.readyState === XHR.OPENED) { - request.log.debug('Request started', {'id':xhr.id}) - for (var key in options.headers) - xhr.setRequestHeader(key, options.headers[key]) - } - - else if(xhr.readyState === XHR.HEADERS_RECEIVED) - on_response() - - else if(xhr.readyState === XHR.LOADING) { - on_response() - on_loading() - } - - else if(xhr.readyState === XHR.DONE) { - on_response() - on_loading() - on_end() - } - } - - function on_response() { - if(did.response) - return - - did.response = true - request.log.debug('Got response', {'id':xhr.id, 'status':xhr.status}) - clearTimeout(xhr.timeoutTimer) - xhr.statusCode = xhr.status // Node request compatibility - - // Detect failed CORS requests. - if(is_cors && xhr.statusCode == 0) { - var cors_err = new Error('CORS request rejected: ' + options.uri) - cors_err.cors = 'rejected' - - // Do not process this request further. - did.loading = true - did.end = true - - return options.callback(cors_err, xhr) - } - - options.onResponse(null, xhr) - } - - function on_loading() { - if(did.loading) - return - - did.loading = true - request.log.debug('Response body loading', {'id':xhr.id}) - // TODO: Maybe simulate "data" events by watching xhr.responseText - } - - function on_end() { - if(did.end) - return - - did.end = true - request.log.debug('Request done', {'id':xhr.id}) - - xhr.body = xhr.responseText - if(options.json) { - try { xhr.body = JSON.parse(xhr.responseText) } - catch (er) { return options.callback(er, xhr) } - } - - options.callback(null, xhr, xhr.body) - } - -} // request - -request.withCredentials = false; -request.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT; - -// -// defaults -// - -request.defaults = function(options, requester) { - var def = function (method) { - var d = function (params, callback) { - if(typeof params === 'string') - params = {'uri': params}; - else { - params = JSON.parse(JSON.stringify(params)); - } - for (var i in options) { - if (params[i] === undefined) params[i] = options[i] - } - return method(params, callback) - } - return d - } - var de = def(request) - de.get = def(request.get) - de.post = def(request.post) - de.put = def(request.put) - de.head = def(request.head) - return de -} - -// -// HTTP method shortcuts -// - -var shortcuts = [ 'get', 'put', 'post', 'head' ]; -shortcuts.forEach(function(shortcut) { - var method = shortcut.toUpperCase(); - var func = shortcut.toLowerCase(); - - request[func] = function(opts) { - if(typeof opts === 'string') - opts = {'method':method, 'uri':opts}; - else { - opts = JSON.parse(JSON.stringify(opts)); - opts.method = method; - } - - var args = [opts].concat(Array.prototype.slice.apply(arguments, [1])); - return request.apply(this, args); - } -}) - -// -// CouchDB shortcut -// - -request.couch = function(options, callback) { - if(typeof options === 'string') - options = {'uri':options} - - // Just use the request API to do JSON. - options.json = true - if(options.body) - options.json = options.body - delete options.body - - callback = callback || noop - - var xhr = request(options, couch_handler) - return xhr - - function couch_handler(er, resp, body) { - if(er) - return callback(er, resp, body) - - if((resp.statusCode < 200 || resp.statusCode > 299) && body.error) { - // The body is a Couch JSON object indicating the error. - er = new Error('CouchDB error: ' + (body.error.reason || body.error.error)) - for (var key in body) - er[key] = body[key] - return callback(er, resp, body); - } - - return callback(er, resp, body); - } -} - -// -// Utility -// - -function noop() {} - -function getLogger() { - var logger = {} - , levels = ['trace', 'debug', 'info', 'warn', 'error'] - , level, i - - for(i = 0; i < levels.length; i++) { - level = levels[i] - - logger[level] = noop - if(typeof console !== 'undefined' && console && console[level]) - logger[level] = formatted(console, level) - } - - return logger -} - -function formatted(obj, method) { - return formatted_logger - - function formatted_logger(str, context) { - if(typeof context === 'object') - str += ' ' + JSON.stringify(context) - - return obj[method].call(obj, str) - } -} - -// Return whether a URL is a cross-domain request. -function is_crossDomain(url) { - // Always return false, we never issue a cross-domain request. - return false; - var rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/ - - // jQuery #8138, IE may throw an exception when accessing - // a field from window.location if document.domain has been set - var ajaxLocation - try { ajaxLocation = location.href } - catch (e) { - // Use the href attribute of an A element since IE will modify it given document.location - ajaxLocation = document.createElement( "a" ); - ajaxLocation.href = ""; - ajaxLocation = ajaxLocation.href; - } - - var ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || [] - , parts = rurl.exec(url.toLowerCase() ) - - var result = !!( - parts && - ( parts[1] != ajaxLocParts[1] - || parts[2] != ajaxLocParts[2] - || (parts[3] || (parts[1] === "http:" ? 80 : 443)) != (ajaxLocParts[3] || (ajaxLocParts[1] === "http:" ? 80 : 443)) - ) - ) - - //console.debug('is_crossDomain('+url+') -> ' + result) - return result -} - -// MIT License from http://phpjs.org/functions/base64_encode:358 -function b64_enc (data) { - // Encodes string using MIME base64 algorithm - var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc="", tmp_arr = []; - - if (!data) { - return data; - } - - // assume utf8 data - // data = this.utf8_encode(data+''); - - do { // pack three octets into four hexets - o1 = data.charCodeAt(i++); - o2 = data.charCodeAt(i++); - o3 = data.charCodeAt(i++); - - bits = o1<<16 | o2<<8 | o3; - - h1 = bits>>18 & 0x3f; - h2 = bits>>12 & 0x3f; - h3 = bits>>6 & 0x3f; - h4 = bits & 0x3f; - - // use hexets to index into b64, and append result to encoded string - tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); - } while (i < data.length); - - enc = tmp_arr.join(''); - - switch (data.length % 3) { - case 1: - enc = enc.slice(0, -2) + '=='; - break; - case 2: - enc = enc.slice(0, -1) + '='; - break; - } - - return enc; -} - return request; -//UMD FOOTER START -})); -//UMD FOOTER END diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/browser-request/LICENSE thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/browser-request/LICENSE --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/browser-request/LICENSE 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/browser-request/LICENSE 1970-01-01 00:00:00.000000000 +0000 @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/content-type/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/content-type/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/content-type/index.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/content-type/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -20,8 +20,8 @@ * obs-text = %x80-FF * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) */ -var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g -var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ +var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g // eslint-disable-line no-control-regex +var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ /** @@ -30,7 +30,7 @@ * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) * obs-text = %x80-FF */ -var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g +var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g // eslint-disable-line no-control-regex /** * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6 @@ -119,7 +119,7 @@ var index = header.indexOf(';') var type = index !== -1 - ? header.substr(0, index).trim() + ? header.slice(0, index).trim() : header.trim() if (!TYPE_REGEXP.test(type)) { @@ -145,11 +145,14 @@ key = match[1].toLowerCase() value = match[2] - if (value[0] === '"') { - // remove quotes and escapes - value = value - .substr(1, value.length - 2) - .replace(QESC_REGEXP, '$1') + if (value.charCodeAt(0) === 0x22 /* " */) { + // remove quotes + value = value.slice(1, -1) + + // remove escapes + if (value.indexOf('\\') !== -1) { + value = value.replace(QESC_REGEXP, '$1') + } } obj.parameters[key] = value diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/@matrix-org/olm/olm.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/@matrix-org/olm/olm.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/@matrix-org/olm/olm.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/@matrix-org/olm/olm.js 2023-04-11 06:11:52.000000000 +0000 @@ -1,12 +1,12 @@ // @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0 -// @source: https://gitlab.matrix.org/matrix-org/olm/-/tree/3.2.12 +// @source: https://gitlab.matrix.org/matrix-org/olm/-/tree/3.2.14 var Olm = (function() { var olm_exports = {}; var onInitSuccess; var onInitFail; -var Module = (function() { +var Module = (() => { var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename; return ( @@ -14,24 +14,24 @@ Module = Module || {}; -var a;a||(a=typeof Module !== 'undefined' ? Module : {});var aa,ba;a.ready=new Promise(function(b,c){aa=b;ba=c});var g;if("undefined"!==typeof window)g=function(b){window.crypto.getRandomValues(b)};else if(module.exports){var ca=require("crypto");g=function(b){var c=ca.randomBytes(b.length);b.set(c)};process=global.process}else throw Error("Cannot find global to attach library to"); -if("undefined"!==typeof OLM_OPTIONS)for(var da in OLM_OPTIONS)OLM_OPTIONS.hasOwnProperty(da)&&(a[da]=OLM_OPTIONS[da]);a.onRuntimeInitialized=function(){h=a._olm_error();olm_exports.PRIVATE_KEY_LENGTH=a._olm_pk_private_key_length();onInitSuccess&&onInitSuccess()};a.onAbort=function(b){onInitFail&&onInitFail(b)};var ea={},l;for(l in a)a.hasOwnProperty(l)&&(ea[l]=a[l]);var ha="object"===typeof window,ia="function"===typeof importScripts,m="",ja,ka,la,n,q; -if("object"===typeof process&&"object"===typeof process.versions&&"string"===typeof process.versions.node)m=ia?require("path").dirname(m)+"/":__dirname+"/",ja=function(b,c){n||(n=require("fs"));q||(q=require("path"));b=q.normalize(b);return n.readFileSync(b,c?null:"utf8")},la=function(b){b=ja(b,!0);b.buffer||(b=new Uint8Array(b));b.buffer||r("Assertion failed: undefined");return b},ka=function(b,c,d){n||(n=require("fs"));q||(q=require("path"));b=q.normalize(b);n.readFile(b,function(e,f){e?d(e):c(f.buffer)})}, -1>0]=0;break;case "i8":u[b>>0]=0;break;case "i16":oa[b>>1]=0;break;case "i32":v[b>>2]=0;break;case "i64":pa=[0,(x=0,1<=+Math.abs(x)?0>>0:~~+Math.ceil((x-+(~~x>>>0))/4294967296)>>>0:0)];v[b>>2]=pa[0];v[b+4>>2]=pa[1];break;case "float":qa[b>>2]=0;break;case "double":ra[b>>3]=0;break;default:r("invalid type for setValue: "+c)}} -function sa(b,c){c=c||"i8";"*"===c.charAt(c.length-1)&&(c="i32");switch(c){case "i1":return u[b>>0];case "i8":return u[b>>0];case "i16":return oa[b>>1];case "i32":return v[b>>2];case "i64":return v[b>>2];case "float":return qa[b>>2];case "double":return Number(ra[b>>3]);default:r("invalid type for getValue: "+c)}return null}var ta,ua=!1,va="undefined"!==typeof TextDecoder?new TextDecoder("utf8"):void 0; -function y(b,c){if(b){var d=z,e=b+c;for(c=b;d[c]&&!(c>=e);)++c;if(16f?e+=String.fromCharCode(f):(f-=65536,e+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else e+=String.fromCharCode(f)}b=e}}else b="";return b} -function A(b,c,d,e){if(!(0=p){var w=b.charCodeAt(++k);p=65536+((p&1023)<<10)|w&1023}if(127>=p){if(d>=e)break;c[d++]=p}else{if(2047>=p){if(d+1>=e)break;c[d++]=192|p>>6}else{if(65535>=p){if(d+2>=e)break;c[d++]=224|p>>12}else{if(d+3>=e)break;c[d++]=240|p>>18;c[d++]=128|p>>12&63}c[d++]=128|p>>6&63}c[d++]=128|p&63}}c[d]=0;return d-f} -function B(b){for(var c=0,d=0;d=e&&(e=65536+((e&1023)<<10)|b.charCodeAt(++d)&1023);127>=e?++c:c=2047>=e?c+2:65535>=e?c+3:c+4}return c}function wa(b,c){for(var d=0;d>0]=b.charCodeAt(d)}var xa,u,z,oa,v,qa,ra; -function ya(){var b=ta.buffer;xa=b;a.HEAP8=u=new Int8Array(b);a.HEAP16=oa=new Int16Array(b);a.HEAP32=v=new Int32Array(b);a.HEAPU8=z=new Uint8Array(b);a.HEAPU16=new Uint16Array(b);a.HEAPU32=new Uint32Array(b);a.HEAPF32=qa=new Float32Array(b);a.HEAPF64=ra=new Float64Array(b)}var za,Aa=[],Ba=[],Da=[];function Ea(){var b=a.preRun.shift();Aa.unshift(b)}var C=0,Fa=null,Ga=null;a.preloadedImages={};a.preloadedAudios={}; -function r(b){if(a.onAbort)a.onAbort(b);b="Aborted("+b+")";ma(b);ua=!0;b=new WebAssembly.RuntimeError(b+". Build with -s ASSERTIONS=1 for more info.");ba(b);throw b;}function Ha(){return D.startsWith("data:application/octet-stream;base64,")}var D;D="olm.wasm";if(!Ha()){var Ia=D;D=a.locateFile?a.locateFile(Ia,m):m+Ia}function Ja(){var b=D;try{if(b==D&&na)return new Uint8Array(na);if(la)return la(b);throw"both async and sync fetching of the wasm failed";}catch(c){r(c)}} -function Ka(){if(!na&&(ha||ia)){if("function"===typeof fetch&&!D.startsWith("file://"))return fetch(D,{credentials:"same-origin"}).then(function(b){if(!b.ok)throw"failed to load wasm binary file at '"+D+"'";return b.arrayBuffer()}).catch(function(){return Ja()});if(ka)return new Promise(function(b,c){ka(D,function(d){b(new Uint8Array(d))},c)})}return Promise.resolve().then(function(){return Ja()})}var x,pa; -function La(b){for(;0=Na.length&&(Na.length=b+1),Na[b]=c=za.get(b));return c} -var Oa={a:function(b,c,d){z.copyWithin(b,c,c+d)},b:function(b){var c=z.length;b>>>=0;if(2147483648=d;d*=2){var e=c*(1+.2/d);e=Math.min(e,b+100663296);e=Math.max(b,e);0>>16);ya();var f=1;break a}catch(k){}f=void 0}if(f)return!0}return!1}}; -(function(){function b(f){a.asm=f.exports;ta=a.asm.c;ya();za=a.asm.e;Ba.unshift(a.asm.d);C--;a.monitorRunDependencies&&a.monitorRunDependencies(C);0==C&&(null!==Fa&&(clearInterval(Fa),Fa=null),Ga&&(f=Ga,Ga=null,f()))}function c(f){b(f.instance)}function d(f){return Ka().then(function(k){return WebAssembly.instantiate(k,e)}).then(function(k){return k}).then(f,function(k){ma("failed to asynchronously prepare wasm: "+k);r(k)})}var e={a:Oa};C++;a.monitorRunDependencies&&a.monitorRunDependencies(C);if(a.instantiateWasm)try{return a.instantiateWasm(e, -b)}catch(f){return ma("Module.instantiateWasm callback failed with error: "+f),!1}(function(){return na||"function"!==typeof WebAssembly.instantiateStreaming||Ha()||D.startsWith("file://")||"function"!==typeof fetch?d(c):fetch(D,{credentials:"same-origin"}).then(function(f){return WebAssembly.instantiateStreaming(f,e).then(c,function(k){ma("wasm streaming compile failed: "+k);ma("falling back to ArrayBuffer instantiation");return d(c)})})})().catch(ba);return{}})(); +var a;a||(a=typeof Module !== 'undefined' ? Module : {});var aa,ca;a.ready=new Promise(function(b,c){aa=b;ca=c});var g;if("undefined"!==typeof window)g=function(b){window.crypto.getRandomValues(b)};else if(module.exports){var da=require("crypto");g=function(b){var c=da.randomBytes(b.length);b.set(c)};process=global.process}else throw Error("Cannot find global to attach library to"); +if("undefined"!==typeof OLM_OPTIONS)for(var ea in OLM_OPTIONS)OLM_OPTIONS.hasOwnProperty(ea)&&(a[ea]=OLM_OPTIONS[ea]);a.onRuntimeInitialized=function(){h=a._olm_error();olm_exports.PRIVATE_KEY_LENGTH=a._olm_pk_private_key_length();onInitSuccess&&onInitSuccess()};a.onAbort=function(b){onInitFail&&onInitFail(b)}; +var fa=Object.assign({},a),ha="object"==typeof window,l="function"==typeof importScripts,ia="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,m="",ja,ka,la,fs,ma,na; +if(ia)m=l?require("path").dirname(m)+"/":__dirname+"/",na=()=>{ma||(fs=require("fs"),ma=require("path"))},ja=function(b,c){na();b=ma.normalize(b);return fs.readFileSync(b,c?void 0:"utf8")},la=b=>{b=ja(b,!0);b.buffer||(b=new Uint8Array(b));return b},ka=(b,c,d)=>{na();b=ma.normalize(b);fs.readFile(b,function(e,f){e?d(e):c(f.buffer)})},1{var c=new XMLHttpRequest;c.open("GET",b,!1);c.send(null);return c.responseText},l&&(la=b=>{var c=new XMLHttpRequest;c.open("GET",b,!1);c.responseType="arraybuffer";c.send(null);return new Uint8Array(c.response)}), +ka=(b,c,d)=>{var e=new XMLHttpRequest;e.open("GET",b,!0);e.responseType="arraybuffer";e.onload=()=>{200==e.status||0==e.status&&e.response?c(e.response):d()};e.onerror=d;e.send(null)};a.print||console.log.bind(console);var n=a.printErr||console.warn.bind(console);Object.assign(a,fa);fa=null;var q;a.wasmBinary&&(q=a.wasmBinary);var noExitRuntime=a.noExitRuntime||!0;"object"!=typeof WebAssembly&&r("no native wasm support detected"); +var oa,pa=!1,qa="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0; +function t(b,c){if(b){var d=u,e=b+c;for(c=b;d[c]&&!(c>=e);)++c;if(16f?e+=String.fromCharCode(f):(f-=65536,e+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else e+=String.fromCharCode(f)}b=e}}else b="";return b} +function v(b,c,d,e){if(!(0=p){var w=b.charCodeAt(++k);p=65536+((p&1023)<<10)|w&1023}if(127>=p){if(d>=e)break;c[d++]=p}else{if(2047>=p){if(d+1>=e)break;c[d++]=192|p>>6}else{if(65535>=p){if(d+2>=e)break;c[d++]=224|p>>12}else{if(d+3>=e)break;c[d++]=240|p>>18;c[d++]=128|p>>12&63}c[d++]=128|p>>6&63}c[d++]=128|p&63}}c[d]=0;return d-f} +function x(b){for(var c=0,d=0;d=e?c++:2047>=e?c+=2:55296<=e&&57343>=e?(c+=4,++d):c+=3}return c}var ra,y,u,sa,z,ta,ua,va;function wa(){var b=oa.buffer;ra=b;a.HEAP8=y=new Int8Array(b);a.HEAP16=sa=new Int16Array(b);a.HEAP32=z=new Int32Array(b);a.HEAPU8=u=new Uint8Array(b);a.HEAPU16=new Uint16Array(b);a.HEAPU32=ta=new Uint32Array(b);a.HEAPF32=ua=new Float32Array(b);a.HEAPF64=va=new Float64Array(b)}var xa=[],za=[],Aa=[]; +function Ba(){var b=a.preRun.shift();xa.unshift(b)}var A=0,Ca=null,B=null;function r(b){if(a.onAbort)a.onAbort(b);b="Aborted("+b+")";n(b);pa=!0;b=new WebAssembly.RuntimeError(b+". Build with -sASSERTIONS for more info.");ca(b);throw b;}function Da(){return C.startsWith("data:application/octet-stream;base64,")}var C;C="olm.wasm";if(!Da()){var Ea=C;C=a.locateFile?a.locateFile(Ea,m):m+Ea} +function Fa(){var b=C;try{if(b==C&&q)return new Uint8Array(q);if(la)return la(b);throw"both async and sync fetching of the wasm failed";}catch(c){r(c)}} +function Ga(){if(!q&&(ha||l)){if("function"==typeof fetch&&!C.startsWith("file://"))return fetch(C,{credentials:"same-origin"}).then(function(b){if(!b.ok)throw"failed to load wasm binary file at '"+C+"'";return b.arrayBuffer()}).catch(function(){return Fa()});if(ka)return new Promise(function(b,c){ka(C,function(d){b(new Uint8Array(d))},c)})}return Promise.resolve().then(function(){return Fa()})}var Ha;function Ia(b){for(;0>0];case "i8":return y[b>>0];case "i16":return sa[b>>1];case "i32":return z[b>>2];case "i64":return z[b>>2];case "float":return ua[b>>2];case "double":return va[b>>3];case "*":return ta[b>>2];default:r("invalid type for getValue: "+c)}return null} +function D(b){var c="i8";c.endsWith("*")&&(c="*");switch(c){case "i1":y[b>>0]=0;break;case "i8":y[b>>0]=0;break;case "i16":sa[b>>1]=0;break;case "i32":z[b>>2]=0;break;case "i64":Ha=[0,0];z[b>>2]=Ha[0];z[b+4>>2]=Ha[1];break;case "float":ua[b>>2]=0;break;case "double":va[b>>3]=0;break;case "*":ta[b>>2]=0;break;default:r("invalid type for setValue: "+c)}}function Ka(b,c,d){for(var e=0;e>0]=b.charCodeAt(e);d||(y[c>>0]=0)} +function La(b,c,d){d=Array(0>>=0;if(2147483648=d;d*=2){var e=c*(1+.2/d);e=Math.min(e,b+100663296);var f=Math;e=Math.max(b,e);f=f.min.call(f,2147483648,e+(65536-e%65536)%65536);a:{try{oa.grow(f-ra.byteLength+65535>>>16);wa();var k=1;break a}catch(p){}k=void 0}if(k)return!0}return!1}}; +(function(){function b(f){a.asm=f.exports;oa=a.asm.c;wa();za.unshift(a.asm.d);A--;a.monitorRunDependencies&&a.monitorRunDependencies(A);0==A&&(null!==Ca&&(clearInterval(Ca),Ca=null),B&&(f=B,B=null,f()))}function c(f){b(f.instance)}function d(f){return Ga().then(function(k){return WebAssembly.instantiate(k,e)}).then(function(k){return k}).then(f,function(k){n("failed to asynchronously prepare wasm: "+k);r(k)})}var e={a:Ma};A++;a.monitorRunDependencies&&a.monitorRunDependencies(A);if(a.instantiateWasm)try{return a.instantiateWasm(e, +b)}catch(f){return n("Module.instantiateWasm callback failed with error: "+f),!1}(function(){return q||"function"!=typeof WebAssembly.instantiateStreaming||Da()||C.startsWith("file://")||ia||"function"!=typeof fetch?d(c):fetch(C,{credentials:"same-origin"}).then(function(f){return WebAssembly.instantiateStreaming(f,e).then(c,function(k){n("wasm streaming compile failed: "+k);n("falling back to ArrayBuffer instantiation");return d(c)})})})().catch(ca);return{}})(); a.___wasm_call_ctors=function(){return(a.___wasm_call_ctors=a.asm.d).apply(null,arguments)};a._olm_get_library_version=function(){return(a._olm_get_library_version=a.asm.f).apply(null,arguments)};a._olm_error=function(){return(a._olm_error=a.asm.g).apply(null,arguments)};a._olm_account_last_error=function(){return(a._olm_account_last_error=a.asm.h).apply(null,arguments)};a.__olm_error_to_string=function(){return(a.__olm_error_to_string=a.asm.i).apply(null,arguments)}; a._olm_account_last_error_code=function(){return(a._olm_account_last_error_code=a.asm.j).apply(null,arguments)};a._olm_session_last_error=function(){return(a._olm_session_last_error=a.asm.k).apply(null,arguments)};a._olm_session_last_error_code=function(){return(a._olm_session_last_error_code=a.asm.l).apply(null,arguments)};a._olm_utility_last_error=function(){return(a._olm_utility_last_error=a.asm.m).apply(null,arguments)}; a._olm_utility_last_error_code=function(){return(a._olm_utility_last_error_code=a.asm.n).apply(null,arguments)};a._olm_account_size=function(){return(a._olm_account_size=a.asm.o).apply(null,arguments)};a._olm_session_size=function(){return(a._olm_session_size=a.asm.p).apply(null,arguments)};a._olm_utility_size=function(){return(a._olm_utility_size=a.asm.q).apply(null,arguments)};a._olm_account=function(){return(a._olm_account=a.asm.r).apply(null,arguments)}; @@ -70,47 +70,47 @@ a._olm_create_sas_random_length=function(){return(a._olm_create_sas_random_length=a.asm.Kb).apply(null,arguments)};a._olm_create_sas=function(){return(a._olm_create_sas=a.asm.Lb).apply(null,arguments)};a._olm_sas_pubkey_length=function(){return(a._olm_sas_pubkey_length=a.asm.Mb).apply(null,arguments)};a._olm_sas_get_pubkey=function(){return(a._olm_sas_get_pubkey=a.asm.Nb).apply(null,arguments)};a._olm_sas_set_their_key=function(){return(a._olm_sas_set_their_key=a.asm.Ob).apply(null,arguments)}; a._olm_sas_is_their_key_set=function(){return(a._olm_sas_is_their_key_set=a.asm.Pb).apply(null,arguments)};a._olm_sas_generate_bytes=function(){return(a._olm_sas_generate_bytes=a.asm.Qb).apply(null,arguments)};a._olm_sas_mac_length=function(){return(a._olm_sas_mac_length=a.asm.Rb).apply(null,arguments)};a._olm_sas_calculate_mac_fixed_base64=function(){return(a._olm_sas_calculate_mac_fixed_base64=a.asm.Sb).apply(null,arguments)}; a._olm_sas_calculate_mac=function(){return(a._olm_sas_calculate_mac=a.asm.Tb).apply(null,arguments)};a._olm_sas_calculate_mac_long_kdf=function(){return(a._olm_sas_calculate_mac_long_kdf=a.asm.Ub).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Vb).apply(null,arguments)};a._free=function(){return(a._free=a.asm.Wb).apply(null,arguments)}; -var Pa=a.stackSave=function(){return(Pa=a.stackSave=a.asm.Xb).apply(null,arguments)},Qa=a.stackRestore=function(){return(Qa=a.stackRestore=a.asm.Yb).apply(null,arguments)},Ra=a.stackAlloc=function(){return(Ra=a.stackAlloc=a.asm.Zb).apply(null,arguments)};a.ALLOC_STACK=1;var Sa;Ga=function Ta(){Sa||Ua();Sa||(Ga=Ta)}; -function Ua(){function b(){if(!Sa&&(Sa=!0,a.calledRun=!0,!ua)){La(Ba);aa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;){var c=a.postRun.shift();Da.unshift(c)}La(Da)}}if(!(0} Resolves to the verified + * @returns Promise which resolves to the verified * configuration, which may include error states. Rejects on unexpected * failure, not when verification fails. */ static async fromDiscoveryConfig(wellknown) { // Step 1 is to get the config, which is provided to us here. + // We default to an error state to make the first few checks easier to // write. We'll update the properties of this object over the duration // of this function. @@ -100,56 +97,49 @@ base_url: null } }; - if (!wellknown || !wellknown["m.homeserver"]) { _logger.logger.error("No m.homeserver key in config"); - clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT; clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID; return Promise.resolve(clientConfig); } - if (!wellknown["m.homeserver"]["base_url"]) { _logger.logger.error("No m.homeserver base_url in config"); - clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT; clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL; return Promise.resolve(clientConfig); - } // Step 2: Make sure the homeserver URL is valid *looking*. We'll make - // sure it points to a homeserver in Step 3. - + } + // Step 2: Make sure the homeserver URL is valid *looking*. We'll make + // sure it points to a homeserver in Step 3. const hsUrl = this.sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]); - if (!hsUrl) { _logger.logger.error("Invalid base_url for m.homeserver"); - clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL; return Promise.resolve(clientConfig); - } // Step 3: Make sure the homeserver URL points to a homeserver. - + } + // Step 3: Make sure the homeserver URL points to a homeserver. const hsVersions = await this.fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`); - - if (!hsVersions || !hsVersions.raw["versions"]) { + if (!hsVersions || !hsVersions.raw?.["versions"]) { _logger.logger.error("Invalid /versions response"); + clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; - clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; // Supply the base_url to the caller because they may be ignoring liveliness + // Supply the base_url to the caller because they may be ignoring liveliness // errors, like this one. - clientConfig["m.homeserver"].base_url = hsUrl; return Promise.resolve(clientConfig); - } // Step 4: Now that the homeserver looks valid, update our client config. - + } + // Step 4: Now that the homeserver looks valid, update our client config. clientConfig["m.homeserver"] = { state: AutoDiscovery.SUCCESS, error: null, base_url: hsUrl - }; // Step 5: Try to pull out the identity server configuration + }; + // Step 5: Try to pull out the identity server configuration let isUrl = ""; - if (wellknown["m.identity_server"]) { // We prepare a failing identity server response to save lines later // in this branch. @@ -160,81 +150,81 @@ error: AutoDiscovery.ERROR_INVALID_IS, base_url: null } - }; // Step 5a: Make sure the URL is valid *looking*. We'll make sure it - // points to an identity server in Step 5b. + }; + // Step 5a: Make sure the URL is valid *looking*. We'll make sure it + // points to an identity server in Step 5b. isUrl = this.sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]); - if (!isUrl) { _logger.logger.error("Invalid base_url for m.identity_server"); - failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IS_BASE_URL; return Promise.resolve(failingClientConfig); - } // Step 5b: Verify there is an identity server listening on the provided - // URL. - - - const isResponse = await this.fetchWellKnownObject(`${isUrl}/_matrix/identity/api/v1`); + } - if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) { - _logger.logger.error("Invalid /api/v1 response"); + // Step 5b: Verify there is an identity server listening on the provided + // URL. + const isResponse = await this.fetchWellKnownObject(`${isUrl}/_matrix/identity/v2`); + if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) { + _logger.logger.error("Invalid /v2 response"); + failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; - failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; // Supply the base_url to the caller because they may be ignoring + // Supply the base_url to the caller because they may be ignoring // liveliness errors, like this one. - failingClientConfig["m.identity_server"].base_url = isUrl; return Promise.resolve(failingClientConfig); } - } // Step 6: Now that the identity server is valid, or never existed, - // populate the IS section. - + } + // Step 6: Now that the identity server is valid, or never existed, + // populate the IS section. if (isUrl && isUrl.toString().length > 0) { clientConfig["m.identity_server"] = { state: AutoDiscovery.SUCCESS, error: null, base_url: isUrl }; - } // Step 7: Copy any other keys directly into the clientConfig. This is for - // things like custom configuration of services. - + } + // Step 7: Copy any other keys directly into the clientConfig. This is for + // things like custom configuration of services. Object.keys(wellknown).forEach(k => { if (k === "m.homeserver" || k === "m.identity_server") { // Only copy selected parts of the config to avoid overwriting // properties computed by the validation logic above. const notProps = ["error", "state", "base_url"]; - for (const prop of Object.keys(wellknown[k])) { if (notProps.includes(prop)) continue; + // @ts-ignore - ts gets unhappy as we're mixing types here clientConfig[k][prop] = wellknown[k][prop]; } } else { // Just copy the whole thing over otherwise clientConfig[k] = wellknown[k]; } - }); // Step 8: Give the config to the caller (finally) + }); + // Step 8: Give the config to the caller (finally) return Promise.resolve(clientConfig); } + /** * Attempts to automatically discover client configuration information * prior to logging in. Such information includes the homeserver URL * and identity server URL the client would want. Additional details * may also be discovered, and will be transparently included in the * response object unaltered. - * @param {string} domain The homeserver domain to perform discovery + * @param domain - The homeserver domain to perform discovery * on. For example, "matrix.org". - * @return {Promise} Resolves to the discovered + * @returns Promise which resolves to the discovered * configuration, which may include error states. Rejects on unexpected * failure, not when discovery fails. */ - - static async findClientConfig(domain) { if (!domain || typeof domain !== "string" || domain.length === 0) { throw new Error("'domain' must be a string of non-zero length"); - } // We use a .well-known lookup for all cases. According to the spec, we + } + + // We use a .well-known lookup for all cases. According to the spec, we // can do other discovery mechanisms if we want such as custom lookups // however we won't bother with that here (mostly because the spec only // supports .well-known right now). @@ -244,11 +234,10 @@ // but will return one anyways (with state PROMPT) to make development // easier for clients. If we can't get a homeserver URL, all bets are // off on the rest of the config and we'll assume it is invalid too. + // We default to an error state to make the first few checks easier to // write. We'll update the properties of this object over the duration // of this function. - - const clientConfig = { "m.homeserver": { state: AutoDiscovery.FAIL_ERROR, @@ -262,16 +251,14 @@ error: null, base_url: null } - }; // Step 1: Actually request the .well-known JSON file and make sure it - // at least has a homeserver definition. + }; + // Step 1: Actually request the .well-known JSON file and make sure it + // at least has a homeserver definition. const wellknown = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`); - if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) { _logger.logger.error("No response or error when parsing .well-known"); - if (wellknown.reason) _logger.logger.error(wellknown.reason); - if (wellknown.action === AutoDiscoveryAction.IGNORE) { clientConfig["m.homeserver"] = { state: AutoDiscovery.PROMPT, @@ -283,71 +270,71 @@ clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT; clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID; } - return Promise.resolve(clientConfig); - } // Step 2: Validate and parse the config - + } + // Step 2: Validate and parse the config return AutoDiscovery.fromDiscoveryConfig(wellknown.raw); } + /** * Gets the raw discovery client configuration for the given domain name. * Should only be used if there's no validation to be done on the resulting * object, otherwise use findClientConfig(). - * @param {string} domain The domain to get the client config for. - * @returns {Promise} Resolves to the domain's client config. Can + * @param domain - The domain to get the client config for. + * @returns Promise which resolves to the domain's client config. Can * be an empty object. */ - - static async getRawClientConfig(domain) { if (!domain || typeof domain !== "string" || domain.length === 0) { throw new Error("'domain' must be a string of non-zero length"); } - const response = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`); if (!response) return {}; return response.raw || {}; } + /** * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and * is suitable for the requirements laid out by .well-known auto discovery. * If valid, the URL will also be stripped of any trailing slashes. - * @param {string} url The potentially invalid URL to sanitize. - * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid. - * @private + * @param url - The potentially invalid URL to sanitize. + * @returns The sanitized URL or a falsey value if the URL is invalid. + * @internal */ - - static sanitizeWellKnownUrl(url) { if (!url) return false; - try { - let parsed = null; - + let parsed; try { parsed = new URL(url); } catch (e) { _logger.logger.error("Could not parse url", e); } - - if (!parsed || !parsed.hostname) return false; + if (!parsed?.hostname) return false; if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false; const port = parsed.port ? `:${parsed.port}` : ""; const path = parsed.pathname ? parsed.pathname : ""; let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`; - if (saferUrl.endsWith("/")) { saferUrl = saferUrl.substring(0, saferUrl.length - 1); } - return saferUrl; } catch (e) { _logger.logger.error(e); - return false; } } + static fetch(resource, options) { + if (this.fetchFn) { + return this.fetchFn(resource, options); + } + return global.fetch(resource, options); + } + static setFetchFn(fetchFn) { + AutoDiscovery.fetchFn = fetchFn; + } + /** * Fetches a JSON object from a given URL, as expected by all .well-known * related lookups. If the server gives a 404 then the `action` will be @@ -360,82 +347,73 @@ * action: One of SUCCESS, IGNORE, or FAIL_PROMPT. * reason: Relatively human-readable description of what went wrong. * error: The actual Error, if one exists. - * @param {string} url The URL to fetch a JSON object from. - * @return {Promise} Resolves to the returned state. - * @private + * @param url - The URL to fetch a JSON object from. + * @returns Promise which resolves to the returned state. + * @internal */ - - - static fetchWellKnownObject(uri) { - return new Promise(resolve => { - // eslint-disable-next-line - const request = require("./matrix").getRequest(); - - if (!request) throw new Error("No request library available"); - request({ - method: "GET", - uri, - timeout: 5000 - }, (error, response, body) => { - if (error || response?.statusCode < 200 || response?.statusCode >= 300) { - const result = { - error, - raw: {} - }; - return resolve(response?.statusCode === 404 ? _objectSpread(_objectSpread({}, result), {}, { - action: AutoDiscoveryAction.IGNORE, - reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN - }) : _objectSpread(_objectSpread({}, result), {}, { - action: AutoDiscoveryAction.FAIL_PROMPT, - reason: error?.message || "General failure" - })); - } - - try { - return resolve({ - raw: JSON.parse(body), - action: AutoDiscoveryAction.SUCCESS - }); - } catch (err) { - return resolve({ - error: err, - raw: {}, - action: AutoDiscoveryAction.FAIL_PROMPT, - reason: err?.name === "SyntaxError" ? AutoDiscovery.ERROR_INVALID_JSON : AutoDiscovery.ERROR_INVALID - }); - } + static async fetchWellKnownObject(url) { + let response; + try { + response = await AutoDiscovery.fetch(url, { + method: _httpApi.Method.Get, + signal: (0, _httpApi.timeoutSignal)(5000) }); - }); + if (response.status === 404) { + return { + raw: {}, + action: AutoDiscoveryAction.IGNORE, + reason: AutoDiscovery.ERROR_MISSING_WELLKNOWN + }; + } + if (!response.ok) { + return { + raw: {}, + action: AutoDiscoveryAction.FAIL_PROMPT, + reason: "General failure" + }; + } + } catch (err) { + const error = err; + let reason = ""; + if (typeof error === "object") { + reason = error?.message; + } + return { + error, + raw: {}, + action: AutoDiscoveryAction.FAIL_PROMPT, + reason: reason || "General failure" + }; + } + try { + return { + raw: await response.json(), + action: AutoDiscoveryAction.SUCCESS + }; + } catch (err) { + const error = err; + return { + error, + raw: {}, + action: AutoDiscoveryAction.FAIL_PROMPT, + reason: error?.name === "SyntaxError" ? AutoDiscovery.ERROR_INVALID_JSON : AutoDiscovery.ERROR_INVALID + }; + } } - } - exports.AutoDiscovery = AutoDiscovery; - -_defineProperty(AutoDiscovery, "ERROR_INVALID", "Invalid homeserver discovery response"); - -_defineProperty(AutoDiscovery, "ERROR_GENERIC_FAILURE", "Failed to get autodiscovery configuration from server"); - -_defineProperty(AutoDiscovery, "ERROR_INVALID_HS_BASE_URL", "Invalid base_url for m.homeserver"); - -_defineProperty(AutoDiscovery, "ERROR_INVALID_HOMESERVER", "Homeserver URL does not appear to be a valid Matrix homeserver"); - -_defineProperty(AutoDiscovery, "ERROR_INVALID_IS_BASE_URL", "Invalid base_url for m.identity_server"); - -_defineProperty(AutoDiscovery, "ERROR_INVALID_IDENTITY_SERVER", "Identity server URL does not appear to be a valid identity server"); - -_defineProperty(AutoDiscovery, "ERROR_INVALID_IS", "Invalid identity server discovery response"); - -_defineProperty(AutoDiscovery, "ERROR_MISSING_WELLKNOWN", "No .well-known JSON file found"); - -_defineProperty(AutoDiscovery, "ERROR_INVALID_JSON", "Invalid JSON"); - -_defineProperty(AutoDiscovery, "ALL_ERRORS", [AutoDiscovery.ERROR_INVALID, AutoDiscovery.ERROR_GENERIC_FAILURE, AutoDiscovery.ERROR_INVALID_HS_BASE_URL, AutoDiscovery.ERROR_INVALID_HOMESERVER, AutoDiscovery.ERROR_INVALID_IS_BASE_URL, AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, AutoDiscovery.ERROR_INVALID_IS, AutoDiscovery.ERROR_MISSING_WELLKNOWN, AutoDiscovery.ERROR_INVALID_JSON]); - +_defineProperty(AutoDiscovery, "ERROR_INVALID", AutoDiscoveryError.Invalid); +_defineProperty(AutoDiscovery, "ERROR_GENERIC_FAILURE", AutoDiscoveryError.GenericFailure); +_defineProperty(AutoDiscovery, "ERROR_INVALID_HS_BASE_URL", AutoDiscoveryError.InvalidHsBaseUrl); +_defineProperty(AutoDiscovery, "ERROR_INVALID_HOMESERVER", AutoDiscoveryError.InvalidHomeserver); +_defineProperty(AutoDiscovery, "ERROR_INVALID_IS_BASE_URL", AutoDiscoveryError.InvalidIsBaseUrl); +_defineProperty(AutoDiscovery, "ERROR_INVALID_IDENTITY_SERVER", AutoDiscoveryError.InvalidIdentityServer); +_defineProperty(AutoDiscovery, "ERROR_INVALID_IS", AutoDiscoveryError.InvalidIs); +_defineProperty(AutoDiscovery, "ERROR_MISSING_WELLKNOWN", AutoDiscoveryError.MissingWellknown); +_defineProperty(AutoDiscovery, "ERROR_INVALID_JSON", AutoDiscoveryError.InvalidJson); +_defineProperty(AutoDiscovery, "ALL_ERRORS", Object.keys(AutoDiscoveryError)); _defineProperty(AutoDiscovery, "FAIL_ERROR", AutoDiscoveryAction.FAIL_ERROR); - _defineProperty(AutoDiscovery, "FAIL_PROMPT", AutoDiscoveryAction.FAIL_PROMPT); - _defineProperty(AutoDiscovery, "PROMPT", AutoDiscoveryAction.PROMPT); - -_defineProperty(AutoDiscovery, "SUCCESS", AutoDiscoveryAction.SUCCESS); \ No newline at end of file +_defineProperty(AutoDiscovery, "SUCCESS", AutoDiscoveryAction.SUCCESS); +_defineProperty(AutoDiscovery, "fetchFn", void 0); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/browser-index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/browser-index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/browser-index.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/browser-index.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,13 +5,7 @@ }); var _exportNames = {}; exports.default = void 0; - -var _browserRequest = _interopRequireDefault(require("browser-request")); - -var _qs = _interopRequireDefault(require("qs")); - var matrixcs = _interopRequireWildcard(require("./matrix")); - Object.keys(matrixcs).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -23,13 +17,8 @@ } }); }); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - /* Copyright 2019 The Matrix.org Foundation C.I.C. @@ -45,38 +34,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -if (matrixcs.getRequest()) { + +if (global.__js_sdk_entrypoint) { throw new Error("Multiple matrix-js-sdk entrypoints detected!"); } +global.__js_sdk_entrypoint = true; -matrixcs.request(function (opts, fn) { - // We manually fix the query string for browser-request because - // it doesn't correctly handle cases like ?via=one&via=two. Instead - // we mimic `request`'s query string interface to make it all work - // as expected. - // browser-request will happily take the constructed string as the - // query string without trying to modify it further. - opts.qs = _qs.default.stringify(opts.qs || {}, opts.qsStringifyOptions); - return (0, _browserRequest.default)(opts, fn); -}); // just *accessing* indexedDB throws an exception in firefox with -// indexeddb disabled. - +// just *accessing* indexedDB throws an exception in firefox with indexeddb disabled. let indexedDB; - try { indexedDB = global.indexedDB; -} catch (e) {} // if our browser (appears to) support indexeddb, use an indexeddb crypto store. - +} catch (e) {} +// if our browser (appears to) support indexeddb, use an indexeddb crypto store. if (indexedDB) { - matrixcs.setCryptoStoreFactory(function () { - return new matrixcs.IndexedDBCryptoStore(indexedDB, "matrix-js-sdk:crypto"); - }); -} // We export 3 things to make browserify happy as well as downstream projects. -// It's awkward, but required. - + matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB, "matrix-js-sdk:crypto")); +} +// We export 3 things to make browserify happy as well as downstream projects. +// It's awkward, but required. var _default = matrixcs; // keep export for browserify package deps - exports.default = _default; global.matrixcs = matrixcs; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/client.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/client.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/client.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/client.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,138 +3,96 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.RoomVersionStability = exports.PendingEventOrdering = exports.MatrixClient = exports.ClientEvent = exports.CRYPTO_ENABLED = void 0; - -var _matrixEventsSdk = require("matrix-events-sdk"); - +exports.UNSTABLE_MSC3852_LAST_SEEN_UA = exports.RoomVersionStability = exports.PendingEventOrdering = exports.MatrixClient = exports.M_AUTHENTICATION = exports.ClientEvent = exports.CRYPTO_ENABLED = void 0; +exports.fixNotificationCountOnDecryption = fixNotificationCountOnDecryption; var _sync = require("./sync"); - var _event = require("./models/event"); - var _stub = require("./store/stub"); - var _call = require("./webrtc/call"); - var _filter = require("./filter"); - var _callEventHandler = require("./webrtc/callEventHandler"); - var utils = _interopRequireWildcard(require("./utils")); - var _eventTimeline = require("./models/event-timeline"); - var _pushprocessor = require("./pushprocessor"); - var _autodiscovery = require("./autodiscovery"); - var olmlib = _interopRequireWildcard(require("./crypto/olmlib")); - var _ReEmitter = require("./ReEmitter"); - var _RoomList = require("./crypto/RoomList"); - var _logger = require("./logger"); - var _serviceTypes = require("./service-types"); - var _httpApi = require("./http-api"); - var _crypto = require("./crypto"); - var _recoverykey = require("./crypto/recoverykey"); - var _key_passphrase = require("./crypto/key_passphrase"); - var _user = require("./models/user"); - var _contentRepo = require("./content-repo"); - var _searchResult = require("./models/search-result"); - var _dehydration = require("./crypto/dehydration"); - -var _matrix = require("./matrix"); - var _api = require("./crypto/api"); - var ContentHelpers = _interopRequireWildcard(require("./content-helpers")); - +var _room = require("./models/room"); +var _roomMember = require("./models/room-member"); var _event2 = require("./@types/event"); - var _partials = require("./@types/partials"); - var _eventMapper = require("./event-mapper"); - var _randomstring = require("./randomstring"); - var _backup = require("./crypto/backup"); - var _MSC3089TreeSpace = require("./models/MSC3089TreeSpace"); - var _search = require("./@types/search"); - var _PushRules = require("./@types/PushRules"); - +var _groupCall = require("./webrtc/groupCall"); var _mediaHandler = require("./webrtc/mediaHandler"); - +var _groupCallEventHandler = require("./webrtc/groupCallEventHandler"); var _typedEventEmitter = require("./models/typed-event-emitter"); - var _read_receipts = require("./@types/read_receipts"); - var _slidingSyncSdk = require("./sliding-sync-sdk"); - var _thread = require("./models/thread"); - var _beacon = require("./@types/beacon"); - var _NamespacedValue = require("./NamespacedValue"); - var _ToDeviceMessageQueue = require("./ToDeviceMessageQueue"); - var _invitesIgnorer = require("./models/invites-ignorer"); - -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - +var _feature = require("./feature"); +var _constants = require("./rust-crypto/constants"); +const _excluded = ["server", "limit", "since"]; +function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } +function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const SCROLLBACK_DELAY_MS = 3000; const CRYPTO_ENABLED = (0, _crypto.isCryptoAvailable)(); exports.CRYPTO_ENABLED = CRYPTO_ENABLED; const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value - const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes +const UNSTABLE_MSC3852_LAST_SEEN_UA = new _NamespacedValue.UnstableValue("last_seen_user_agent", "org.matrix.msc3852.last_seen_user_agent"); +exports.UNSTABLE_MSC3852_LAST_SEEN_UA = UNSTABLE_MSC3852_LAST_SEEN_UA; let PendingEventOrdering; exports.PendingEventOrdering = PendingEventOrdering; - (function (PendingEventOrdering) { PendingEventOrdering["Chronological"] = "chronological"; PendingEventOrdering["Detached"] = "detached"; })(PendingEventOrdering || (exports.PendingEventOrdering = PendingEventOrdering = {})); - let RoomVersionStability; exports.RoomVersionStability = RoomVersionStability; - (function (RoomVersionStability) { RoomVersionStability["Stable"] = "stable"; RoomVersionStability["Unstable"] = "unstable"; })(RoomVersionStability || (exports.RoomVersionStability = RoomVersionStability = {})); - var CrossSigningKeyType; - (function (CrossSigningKeyType) { CrossSigningKeyType["MasterKey"] = "master_key"; CrossSigningKeyType["SelfSigningKey"] = "self_signing_key"; CrossSigningKeyType["UserSigningKey"] = "user_signing_key"; })(CrossSigningKeyType || (CrossSigningKeyType = {})); - +const M_AUTHENTICATION = new _NamespacedValue.UnstableValue("m.authentication", "org.matrix.msc2965.authentication"); +exports.M_AUTHENTICATION = M_AUTHENTICATION; /* eslint-enable camelcase */ + // We're using this constant for methods overloading and inspect whether a variable // contains an eventId or not. This was required to ensure backwards compatibility // of methods for threads @@ -142,7 +100,6 @@ const EVENT_ID_PREFIX = "$"; let ClientEvent; exports.ClientEvent = ClientEvent; - (function (ClientEvent) { ClientEvent["Sync"] = "sync"; ClientEvent["Event"] = "event"; @@ -152,218 +109,186 @@ ClientEvent["DeleteRoom"] = "deleteRoom"; ClientEvent["SyncUnexpectedError"] = "sync.unexpectedError"; ClientEvent["ClientWellKnown"] = "WellKnown.client"; + ClientEvent["ReceivedVoipEvent"] = "received_voip_event"; + ClientEvent["UndecryptableToDeviceEvent"] = "toDeviceEvent.undecryptable"; ClientEvent["TurnServers"] = "turnServers"; ClientEvent["TurnServersError"] = "turnServers.error"; })(ClientEvent || (exports.ClientEvent = ClientEvent = {})); - const SSO_ACTION_PARAM = new _NamespacedValue.UnstableValue("action", "org.matrix.msc3824.action"); + /** * Represents a Matrix Client. Only directly construct this if you want to use * custom modules. Normally, {@link createClient} should be used * as it specifies 'sensible' defaults for these modules. */ - class MatrixClient extends _typedEventEmitter.TypedEventEmitter { // populated after initCrypto + // XXX: Intended private, used in code. + // libolm crypto implementation. XXX: Intended private, used in code. Being replaced by cryptoBackend + // one of crypto or rustCrypto // XXX: Intended private, used in code. // XXX: Intended private, used in code. + // XXX: Intended private, used in code. // XXX: Intended private, used in code. // XXX: Intended private, used in code. - // XXX: Intended private, used in code. + // Note: these are all `protected` to let downstream consumers make mistakes if they want to. // We don't technically support this usage, but have reasons to do this. + // The pushprocessor caches useful things, so keep one and re-use it + // Promise to a response of the server's /versions response // TODO: This should expire: https://github.com/matrix-org/matrix-js-sdk/issues/1020 + // A manager for determining which invites should be ignored. + constructor(opts) { super(); - _defineProperty(this, "reEmitter", new _ReEmitter.TypedReEmitter(this)); - _defineProperty(this, "olmVersion", null); - _defineProperty(this, "usingExternalCrypto", false); - _defineProperty(this, "store", void 0); - _defineProperty(this, "deviceId", void 0); - _defineProperty(this, "credentials", void 0); - _defineProperty(this, "pickleKey", void 0); - _defineProperty(this, "scheduler", void 0); - _defineProperty(this, "clientRunning", false); - _defineProperty(this, "timelineSupport", false); - _defineProperty(this, "urlPreviewCache", {}); - _defineProperty(this, "identityServer", void 0); - _defineProperty(this, "http", void 0); - _defineProperty(this, "crypto", void 0); - + _defineProperty(this, "cryptoBackend", void 0); _defineProperty(this, "cryptoCallbacks", void 0); - _defineProperty(this, "callEventHandler", void 0); - + _defineProperty(this, "groupCallEventHandler", void 0); _defineProperty(this, "supportsCallTransfer", false); - _defineProperty(this, "forceTURN", false); - _defineProperty(this, "iceCandidatePoolSize", 0); - _defineProperty(this, "idBaseUrl", void 0); - _defineProperty(this, "baseUrl", void 0); - + _defineProperty(this, "isVoipWithNoMediaAllowed", void 0); _defineProperty(this, "canSupportVoip", false); - _defineProperty(this, "peekSync", null); - _defineProperty(this, "isGuestAccount", false); - _defineProperty(this, "ongoingScrollbacks", {}); - _defineProperty(this, "notifTimelineSet", null); - _defineProperty(this, "cryptoStore", void 0); - _defineProperty(this, "verificationMethods", void 0); - _defineProperty(this, "fallbackICEServerAllowed", false); - _defineProperty(this, "roomList", void 0); - _defineProperty(this, "syncApi", void 0); - _defineProperty(this, "roomNameGenerator", void 0); - _defineProperty(this, "pushRules", void 0); - _defineProperty(this, "syncLeftRoomsPromise", void 0); - _defineProperty(this, "syncedLeftRooms", false); - _defineProperty(this, "clientOpts", void 0); - _defineProperty(this, "clientWellKnownIntervalID", void 0); - _defineProperty(this, "canResetTimelineCallback", void 0); - + _defineProperty(this, "canSupport", new Map()); _defineProperty(this, "pushProcessor", new _pushprocessor.PushProcessor(this)); - _defineProperty(this, "serverVersionsPromise", void 0); - _defineProperty(this, "cachedCapabilities", void 0); - _defineProperty(this, "clientWellKnown", void 0); - _defineProperty(this, "clientWellKnownPromise", void 0); - _defineProperty(this, "turnServers", []); - _defineProperty(this, "turnServersExpiry", 0); - - _defineProperty(this, "checkTurnServersIntervalID", null); - + _defineProperty(this, "checkTurnServersIntervalID", void 0); _defineProperty(this, "exportedOlmDeviceToImport", void 0); - _defineProperty(this, "txnCtr", 0); - _defineProperty(this, "mediaHandler", new _mediaHandler.MediaHandler(this)); - + _defineProperty(this, "sessionId", void 0); _defineProperty(this, "pendingEventEncryption", new Map()); - + _defineProperty(this, "useE2eForGroupCall", true); _defineProperty(this, "toDeviceMessageQueue", void 0); - _defineProperty(this, "ignoredInvites", void 0); - _defineProperty(this, "startCallEventHandler", () => { if (this.isInitialSyncComplete()) { this.callEventHandler.start(); + this.groupCallEventHandler.start(); this.off(ClientEvent.Sync, this.startCallEventHandler); } }); - + _defineProperty(this, "fixupRoomNotifications", () => { + if (this.isInitialSyncComplete()) { + const unreadRooms = (this.getRooms() ?? []).filter(room => { + return room.getUnreadNotificationCount(_room.NotificationCountType.Total) > 0; + }); + for (const room of unreadRooms) { + const currentUserId = this.getSafeUserId(); + room.fixupNotifications(currentUserId); + } + this.off(ClientEvent.Sync, this.fixupRoomNotifications); + } + }); opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl); opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl); this.baseUrl = opts.baseUrl; this.idBaseUrl = opts.idBaseUrl; this.identityServer = opts.identityServer; - this.usingExternalCrypto = opts.usingExternalCrypto; + this.usingExternalCrypto = opts.usingExternalCrypto ?? false; this.store = opts.store || new _stub.StubStore(); this.deviceId = opts.deviceId || null; + this.sessionId = (0, _randomstring.randomString)(10); const userId = opts.userId || null; this.credentials = { userId }; this.http = new _httpApi.MatrixHttpApi(this, { + fetchFn: opts.fetchFn, baseUrl: opts.baseUrl, idBaseUrl: opts.idBaseUrl, accessToken: opts.accessToken, - request: opts.request, - prefix: _httpApi.PREFIX_R0, + prefix: _httpApi.ClientPrefix.R0, onlyData: true, extraParams: opts.queryParams, localTimeoutMs: opts.localTimeoutMs, useAuthorizationHeader: opts.useAuthorizationHeader }); - if (opts.deviceToImport) { if (this.deviceId) { - _logger.logger.warn('not importing device because device ID is provided to ' + 'constructor independently of exported data'); + _logger.logger.warn("not importing device because device ID is provided to " + "constructor independently of exported data"); } else if (this.credentials.userId) { - _logger.logger.warn('not importing device because user ID is provided to ' + 'constructor independently of exported data'); + _logger.logger.warn("not importing device because user ID is provided to " + "constructor independently of exported data"); } else if (!opts.deviceToImport.deviceId) { - _logger.logger.warn('not importing device because no device ID in exported data'); + _logger.logger.warn("not importing device because no device ID in exported data"); } else { this.deviceId = opts.deviceToImport.deviceId; - this.credentials.userId = opts.deviceToImport.userId; // will be used during async initialization of the crypto - + this.credentials.userId = opts.deviceToImport.userId; + // will be used during async initialization of the crypto this.exportedOlmDeviceToImport = opts.deviceToImport.olmDevice; } } else if (opts.pickleKey) { this.pickleKey = opts.pickleKey; } - this.scheduler = opts.scheduler; - if (this.scheduler) { this.scheduler.setProcessFunction(async eventToSend => { const room = this.getRoom(eventToSend.getRoomId()); - if (eventToSend.status !== _event.EventStatus.SENDING) { this.updatePendingEventStatus(room, eventToSend, _event.EventStatus.SENDING); } - const res = await this.sendEventHttpRequest(eventToSend); - if (room) { // ensure we update pending event before the next scheduler run so that any listeners to event id // updates on the synchronous event emitter get a chance to run first. room.updatePendingEvent(eventToSend, _event.EventStatus.SENT, res.event_id); } - return res; }); } - if ((0, _call.supportsMatrixCall)()) { this.callEventHandler = new _callEventHandler.CallEventHandler(this); - this.canSupportVoip = true; // Start listening for calls after the initial sync is done + this.groupCallEventHandler = new _groupCallEventHandler.GroupCallEventHandler(this); + this.canSupportVoip = true; + // Start listening for calls after the initial sync is done // We do not need to backfill the call event buffer // with encrypted events that might never get decrypted - this.on(ClientEvent.Sync, this.startCallEventHandler); } - + this.on(ClientEvent.Sync, this.fixupRoomNotifications); this.timelineSupport = Boolean(opts.timelineSupport); this.cryptoStore = opts.cryptoStore; this.verificationMethods = opts.verificationMethods; @@ -371,50 +296,29 @@ this.forceTURN = opts.forceTURN || false; this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize; this.supportsCallTransfer = opts.supportsCallTransfer || false; - this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false; // List of which rooms have encryption enabled: separate from crypto because + this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false; + this.isVoipWithNoMediaAllowed = opts.isVoipWithNoMediaAllowed || false; + if (opts.useE2eForGroupCall !== undefined) this.useE2eForGroupCall = opts.useE2eForGroupCall; + + // List of which rooms have encryption enabled: separate from crypto because // we still want to know which rooms are encrypted even if crypto is disabled: // we don't want to start sending unencrypted events to them. - this.roomList = new _RoomList.RoomList(this.cryptoStore); this.roomNameGenerator = opts.roomNameGenerator; - this.toDeviceMessageQueue = new _ToDeviceMessageQueue.ToDeviceMessageQueue(this); // The SDK doesn't really provide a clean way for events to recalculate the push + this.toDeviceMessageQueue = new _ToDeviceMessageQueue.ToDeviceMessageQueue(this); + + // The SDK doesn't really provide a clean way for events to recalculate the push // actions for themselves, so we have to kinda help them out when they are encrypted. // We do this so that push rules are correctly executed on events in their decrypted // state, such as highlights when the user's name is mentioned. - this.on(_event.MatrixEventEvent.Decrypted, event => { - const oldActions = event.getPushActions(); - const actions = this.getPushActionsForEvent(event, true); - const room = this.getRoom(event.getRoomId()); - if (!room) return; - const currentCount = room.getUnreadNotificationCount(_matrix.NotificationCountType.Highlight); // Ensure the unread counts are kept up to date if the event is encrypted - // We also want to make sure that the notification count goes up if we already - // have encrypted events to avoid other code from resetting 'highlight' to zero. - - const oldHighlight = !!oldActions?.tweaks?.highlight; - const newHighlight = !!actions?.tweaks?.highlight; - - if (oldHighlight !== newHighlight || currentCount > 0) { - // TODO: Handle mentions received while the client is offline - // See also https://github.com/vector-im/element-web/issues/9069 - if (!room.hasUserReadEvent(this.getUserId(), event.getId())) { - let newCount = currentCount; - if (newHighlight && !oldHighlight) newCount++; - if (!newHighlight && oldHighlight) newCount--; - room.setUnreadNotificationCount(_matrix.NotificationCountType.Highlight, newCount); // Fix 'Mentions Only' rooms from not having the right badge count - - const totalCount = room.getUnreadNotificationCount(_matrix.NotificationCountType.Total); + fixNotificationCountOnDecryption(this, event); + }); - if (totalCount < newCount) { - room.setUnreadNotificationCount(_matrix.NotificationCountType.Total, newCount); - } - } - } - }); // Like above, we have to listen for read receipts from ourselves in order to + // Like above, we have to listen for read receipts from ourselves in order to // correctly handle notification counts on encrypted rooms. // This fixes https://github.com/vector-im/element-web/issues/9421 - - this.on(_matrix.RoomEvent.Receipt, (event, room) => { + this.on(_room.RoomEvent.Receipt, (event, room) => { if (room && this.isRoomEncrypted(room.roomId)) { // Figure out if we've read something or if it's just informational const content = event.getContent(); @@ -424,224 +328,205 @@ if (!value) continue; if (Object.keys(value).includes(this.getUserId())) return true; } - return false; }).length > 0; - if (!isSelf) return; // Work backwards to determine how many events are unread. We also set + if (!isSelf) return; + + // Work backwards to determine how many events are unread. We also set // a limit for how back we'll look to avoid spinning CPU for too long. // If we hit the limit, we assume the count is unchanged. - const maxHistory = 20; const events = room.getLiveTimeline().getEvents(); let highlightCount = 0; - for (let i = events.length - 1; i >= 0; i--) { if (i === events.length - maxHistory) return; // limit reached const event = events[i]; - if (room.hasUserReadEvent(this.getUserId(), event.getId())) { // If the user has read the event, then the counting is done. break; } - const pushActions = this.getPushActionsForEvent(event); - highlightCount += pushActions.tweaks && pushActions.tweaks.highlight ? 1 : 0; - } // Note: we don't need to handle 'total' notifications because the counts - // will come from the server. - + highlightCount += pushActions?.tweaks?.highlight ? 1 : 0; + } - room.setUnreadNotificationCount(_matrix.NotificationCountType.Highlight, highlightCount); + // Note: we don't need to handle 'total' notifications because the counts + // will come from the server. + room.setUnreadNotificationCount(_room.NotificationCountType.Highlight, highlightCount); } }); this.ignoredInvites = new _invitesIgnorer.IgnoredInvites(this); } + /** * High level helper method to begin syncing and poll for new events. To listen for these - * events, add a listener for {@link module:client~MatrixClient#event:"event"} - * via {@link module:client~MatrixClient#on}. Alternatively, listen for specific + * events, add a listener for {@link ClientEvent.Event} + * via {@link MatrixClient#on}. Alternatively, listen for specific * state change events. - * @param {Object=} opts Options to apply when syncing. + * @param opts - Options to apply when syncing. */ - - async startClient(opts) { if (this.clientRunning) { // client is already running. return; } - - this.clientRunning = true; // backwards compat for when 'opts' was 'historyLen'. - + this.clientRunning = true; + // backwards compat for when 'opts' was 'historyLen'. if (typeof opts === "number") { opts = { initialSyncLimit: opts }; - } // Create our own user object artificially (instead of waiting for sync) - // so it's always available, even if the user is not in any rooms etc. - + } + // Create our own user object artificially (instead of waiting for sync) + // so it's always available, even if the user is not in any rooms etc. const userId = this.getUserId(); - if (userId) { this.store.storeUser(new _user.User(userId)); } - if (this.crypto) { - this.crypto.uploadDeviceKeys(); - this.crypto.start(); - } // periodically poll for turn servers if we support voip - - + // periodically poll for turn servers if we support voip if (this.canSupportVoip) { this.checkTurnServersIntervalID = setInterval(() => { this.checkTurnServers(); - }, TURN_CHECK_INTERVAL); // noinspection ES6MissingAwait - + }, TURN_CHECK_INTERVAL); + // noinspection ES6MissingAwait this.checkTurnServers(); } - if (this.syncApi) { // This shouldn't happen since we thought the client was not running _logger.logger.error("Still have sync object whilst not running: stopping old one"); - this.syncApi.stop(); } - try { + await this.getVersions(); + + // This should be done with `canSupport` + // TODO: https://github.com/vector-im/element-web/issues/23643 const { - serverSupport, - stable + threads, + list, + fwdPagination } = await this.doesServerSupportThread(); - - _thread.Thread.setServerSideSupport(serverSupport, stable); + _thread.Thread.setServerSideSupport(threads); + _thread.Thread.setServerSideListSupport(list); + _thread.Thread.setServerSideFwdPaginationSupport(fwdPagination); } catch (e) { - // Most likely cause is that `doesServerSupportThread` returned `null` (as it - // is allowed to do) and thus we enter "degraded mode" on threads. - _thread.Thread.setServerSideSupport(false, true); - } // shallow-copy the opts dict before modifying and storing it - - - this.clientOpts = Object.assign({}, opts); - this.clientOpts.crypto = this.crypto; - - this.clientOpts.canResetEntireTimeline = roomId => { - if (!this.canResetTimelineCallback) { - return false; - } - - return this.canResetTimelineCallback(roomId); - }; - + _logger.logger.error("Can't fetch server versions, continuing to initialise sync, this will be retried later", e); + } + this.clientOpts = opts ?? {}; if (this.clientOpts.slidingSync) { - this.syncApi = new _slidingSyncSdk.SlidingSyncSdk(this.clientOpts.slidingSync, this, this.clientOpts); + this.syncApi = new _slidingSyncSdk.SlidingSyncSdk(this.clientOpts.slidingSync, this, this.clientOpts, this.buildSyncApiOptions()); } else { - this.syncApi = new _sync.SyncApi(this, this.clientOpts); + this.syncApi = new _sync.SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); + } + if (this.clientOpts.hasOwnProperty("experimentalThreadSupport")) { + _logger.logger.warn("`experimentalThreadSupport` has been deprecated, use `threadSupport` instead"); } + // If `threadSupport` is omitted and the deprecated `experimentalThreadSupport` has been passed + // We should fallback to that value for backwards compatibility purposes + if (!this.clientOpts.hasOwnProperty("threadSupport") && this.clientOpts.hasOwnProperty("experimentalThreadSupport")) { + this.clientOpts.threadSupport = this.clientOpts.experimentalThreadSupport; + } this.syncApi.sync(); - if (this.clientOpts.clientWellKnownPollPeriod !== undefined) { this.clientWellKnownIntervalID = setInterval(() => { this.fetchClientWellKnown(); }, 1000 * this.clientOpts.clientWellKnownPollPeriod); this.fetchClientWellKnown(); } - this.toDeviceMessageQueue.start(); } + + /** + * Construct a SyncApiOptions for this client, suitable for passing into the SyncApi constructor + */ + buildSyncApiOptions() { + return { + crypto: this.crypto, + cryptoCallbacks: this.cryptoBackend, + canResetEntireTimeline: roomId => { + if (!this.canResetTimelineCallback) { + return false; + } + return this.canResetTimelineCallback(roomId); + } + }; + } + /** * High level helper method to stop the client from polling and allow a * clean shutdown. */ - - stopClient() { - this.crypto?.stop(); // crypto might have been initialised even if the client wasn't fully started + this.cryptoBackend?.stop(); // crypto might have been initialised even if the client wasn't fully started if (!this.clientRunning) return; // already stopped - _logger.logger.log('stopping MatrixClient'); - + _logger.logger.log("stopping MatrixClient"); this.clientRunning = false; this.syncApi?.stop(); - this.syncApi = null; + this.syncApi = undefined; this.peekSync?.stopPeeking(); this.callEventHandler?.stop(); - this.callEventHandler = null; + this.groupCallEventHandler?.stop(); + this.callEventHandler = undefined; + this.groupCallEventHandler = undefined; global.clearInterval(this.checkTurnServersIntervalID); - this.checkTurnServersIntervalID = null; - + this.checkTurnServersIntervalID = undefined; if (this.clientWellKnownIntervalID !== undefined) { global.clearInterval(this.clientWellKnownIntervalID); } - this.toDeviceMessageQueue.stop(); } + /** * Try to rehydrate a device if available. The client must have been * initialized with a `cryptoCallback.getDehydrationKey` option, and this * function must be called before initCrypto and startClient are called. * - * @return {Promise} Resolves to undefined if a device could not be dehydrated, or + * @returns Promise which resolves to undefined if a device could not be dehydrated, or * to the new device ID if the dehydration was successful. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Rejects: with an error response. */ - - async rehydrateDevice() { if (this.crypto) { throw new Error("Cannot rehydrate device after crypto is initialized"); } - if (!this.cryptoCallbacks.getDehydrationKey) { return; } - const getDeviceResult = await this.getDehydratedDevice(); - if (!getDeviceResult) { return; } - if (!getDeviceResult.device_data || !getDeviceResult.device_id) { _logger.logger.info("no dehydrated device found"); - return; } - const account = new global.Olm.Account(); - try { const deviceData = getDeviceResult.device_data; - if (deviceData.algorithm !== _dehydration.DEHYDRATION_ALGORITHM) { _logger.logger.warn("Wrong algorithm for dehydrated device"); - return; } - _logger.logger.log("unpickling dehydrated device"); - const key = await this.cryptoCallbacks.getDehydrationKey(deviceData, k => { // copy the key so that it doesn't get clobbered account.unpickle(new Uint8Array(k), deviceData.account); }); account.unpickle(key, deviceData.account); - _logger.logger.log("unpickled device"); - - const rehydrateResult = await this.http.authedRequest(undefined, _httpApi.Method.Post, "/dehydrated_device/claim", undefined, { + const rehydrateResult = await this.http.authedRequest(_httpApi.Method.Post, "/dehydrated_device/claim", undefined, { device_id: getDeviceResult.device_id }, { prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2" }); - - if (rehydrateResult.success === true) { + if (rehydrateResult.success) { this.deviceId = getDeviceResult.device_id; - _logger.logger.info("using dehydrated device"); - const pickleKey = this.pickleKey || "DEFAULT_KEY"; this.exportedOlmDeviceToImport = { pickledAccount: account.pickle(pickleKey), @@ -652,85 +537,71 @@ return this.deviceId; } else { account.free(); - _logger.logger.info("not using dehydrated device"); - return; } } catch (e) { account.free(); - _logger.logger.warn("could not unpickle", e); } } + /** * Get the current dehydrated device, if any - * @return {Promise} A promise of an object containing the dehydrated device + * @returns A promise of an object containing the dehydrated device */ - - async getDehydratedDevice() { try { - return await this.http.authedRequest(undefined, _httpApi.Method.Get, "/dehydrated_device", undefined, undefined, { + return await this.http.authedRequest(_httpApi.Method.Get, "/dehydrated_device", undefined, undefined, { prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2" }); } catch (e) { - _logger.logger.info("could not get dehydrated device", e.toString()); - + _logger.logger.info("could not get dehydrated device", e); return; } } + /** * Set the dehydration key. This will also periodically dehydrate devices to * the server. * - * @param {Uint8Array} key the dehydration key - * @param {IDehydratedDeviceKeyInfo} [keyInfo] Information about the key. Primarily for + * @param key - the dehydration key + * @param keyInfo - Information about the key. Primarily for * information about how to generate the key from a passphrase. - * @param {string} [deviceDisplayName] The device display name for the + * @param deviceDisplayName - The device display name for the * dehydrated device. - * @return {Promise} A promise that resolves when the dehydrated device is stored. + * @returns A promise that resolves when the dehydrated device is stored. */ - - - setDehydrationKey(key, keyInfo, deviceDisplayName) { + async setDehydrationKey(key, keyInfo, deviceDisplayName) { if (!this.crypto) { - _logger.logger.warn('not dehydrating device if crypto is not enabled'); - + _logger.logger.warn("not dehydrating device if crypto is not enabled"); return; } - return this.crypto.dehydrationManager.setKeyAndQueueDehydration(key, keyInfo, deviceDisplayName); } + /** * Creates a new dehydrated device (without queuing periodic dehydration) - * @param {Uint8Array} key the dehydration key - * @param {IDehydratedDeviceKeyInfo} [keyInfo] Information about the key. Primarily for + * @param key - the dehydration key + * @param keyInfo - Information about the key. Primarily for * information about how to generate the key from a passphrase. - * @param {string} [deviceDisplayName] The device display name for the + * @param deviceDisplayName - The device display name for the * dehydrated device. - * @return {Promise} the device id of the newly created dehydrated device + * @returns the device id of the newly created dehydrated device */ - - async createDehydratedDevice(key, keyInfo, deviceDisplayName) { if (!this.crypto) { - _logger.logger.warn('not dehydrating device if crypto is not enabled'); - + _logger.logger.warn("not dehydrating device if crypto is not enabled"); return; } - await this.crypto.dehydrationManager.setKey(key, keyInfo, deviceDisplayName); return this.crypto.dehydrationManager.dehydrateDevice(); } - async exportDevice() { if (!this.crypto) { - _logger.logger.warn('not exporting device if crypto is not enabled'); - + _logger.logger.warn("not exporting device if crypto is not enabled"); return; } - return { userId: this.credentials.userId, deviceId: this.deviceId, @@ -738,187 +609,267 @@ olmDevice: await this.crypto.olmDevice.export() }; } + /** * Clear any data out of the persistent stores used by the client. * - * @returns {Promise} Promise which resolves when the stores have been cleared. + * @returns Promise which resolves when the stores have been cleared. */ - - clearStores() { if (this.clientRunning) { throw new Error("Cannot clear stores while client is running"); } - const promises = []; promises.push(this.store.deleteAllData()); - if (this.cryptoStore) { promises.push(this.cryptoStore.deleteAllData()); } - return Promise.all(promises).then(); // .then to fix types + // delete the stores used by the rust matrix-sdk-crypto, in case they were used + const deleteRustSdkStore = async () => { + let indexedDB; + try { + indexedDB = global.indexedDB; + } catch (e) { + // No indexeddb support + return; + } + for (const dbname of [`${_constants.RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto`, `${_constants.RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto-meta`]) { + const prom = new Promise((resolve, reject) => { + _logger.logger.info(`Removing IndexedDB instance ${dbname}`); + const req = indexedDB.deleteDatabase(dbname); + req.onsuccess = _ => { + _logger.logger.info(`Removed IndexedDB instance ${dbname}`); + resolve(0); + }; + req.onerror = e => { + // In private browsing, Firefox has a global.indexedDB, but attempts to delete an indexeddb + // (even a non-existent one) fail with "DOMException: A mutation operation was attempted on a + // database that did not allow mutations." + // + // it seems like the only thing we can really do is ignore the error. + _logger.logger.warn(`Failed to remove IndexedDB instance ${dbname}:`, e); + resolve(0); + }; + req.onblocked = e => { + _logger.logger.info(`cannot yet remove IndexedDB instance ${dbname}`); + }; + }); + await prom; + } + }; + promises.push(deleteRustSdkStore()); + return Promise.all(promises).then(); // .then to fix types } + /** * Get the user-id of the logged-in user * - * @return {?string} MXID for the logged-in user, or null if not logged in + * @returns MXID for the logged-in user, or null if not logged in */ - - getUserId() { if (this.credentials && this.credentials.userId) { return this.credentials.userId; } - return null; } + + /** + * Get the user-id of the logged-in user + * + * @returns MXID for the logged-in user + * @throws Error if not logged in + */ + getSafeUserId() { + const userId = this.getUserId(); + if (!userId) { + throw new Error("Expected logged in user but found none."); + } + return userId; + } + /** * Get the domain for this client's MXID - * @return {?string} Domain of this MXID + * @returns Domain of this MXID */ - - getDomain() { if (this.credentials && this.credentials.userId) { - return this.credentials.userId.replace(/^.*?:/, ''); + return this.credentials.userId.replace(/^.*?:/, ""); } - return null; } + /** - * Get the local part of the current user ID e.g. "foo" in "@foo:bar". - * @return {?string} The user ID localpart or null. + * Get the local part of the current user ID e.g. "foo" in "\@foo:bar". + * @returns The user ID localpart or null. */ - - getUserIdLocalpart() { if (this.credentials && this.credentials.userId) { return this.credentials.userId.split(":")[0].substring(1); } - return null; } + /** * Get the device ID of this client - * @return {?string} device ID + * @returns device ID */ - - getDeviceId() { return this.deviceId; } + /** - * Check if the runtime environment supports VoIP calling. - * @return {boolean} True if VoIP is supported. + * Get the session ID of this client + * @returns session ID */ + getSessionId() { + return this.sessionId; + } - + /** + * Check if the runtime environment supports VoIP calling. + * @returns True if VoIP is supported. + */ supportsVoip() { return this.canSupportVoip; } + /** - * @returns {MediaHandler} + * @returns */ - - getMediaHandler() { return this.mediaHandler; } + /** * Set whether VoIP calls are forced to use only TURN * candidates. This is the same as the forceTURN option * when creating the client. - * @param {boolean} force True to force use of TURN servers + * @param force - True to force use of TURN servers */ - - setForceTURN(force) { this.forceTURN = force; } + /** * Set whether to advertise transfer support to other parties on Matrix calls. - * @param {boolean} support True to advertise the 'm.call.transferee' capability + * @param support - True to advertise the 'm.call.transferee' capability */ - - setSupportsCallTransfer(support) { this.supportsCallTransfer = support; } + + /** + * Returns true if to-device signalling for group calls will be encrypted with Olm. + * If false, it will be sent unencrypted. + * @returns boolean Whether group call signalling will be encrypted + */ + getUseE2eForGroupCall() { + return this.useE2eForGroupCall; + } + /** * Creates a new call. * The place*Call methods on the returned call can be used to actually place a call * - * @param {string} roomId The room the call is to be placed in. - * @return {MatrixCall} the call or null if the browser doesn't support calling. + * @param roomId - The room the call is to be placed in. + * @returns the call or null if the browser doesn't support calling. */ - - createCall(roomId) { return (0, _call.createNewMatrixCall)(this, roomId); } + /** - * Get the current sync state. - * @return {?SyncState} the sync state, which may be null. - * @see module:client~MatrixClient#event:"sync" + * Creates a new group call and sends the associated state event + * to alert other members that the room now has a group call. + * + * @param roomId - The room the call is to be placed in. + */ + async createGroupCall(roomId, type, isPtt, intent, dataChannelsEnabled, dataChannelOptions) { + if (this.getGroupCallForRoom(roomId)) { + throw new Error(`${roomId} already has an existing group call`); + } + const room = this.getRoom(roomId); + if (!room) { + throw new Error(`Cannot find room ${roomId}`); + } + + // Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a + // no media WebRTC connection anyway. + return new _groupCall.GroupCall(this, room, type, isPtt, intent, undefined, dataChannelsEnabled || this.isVoipWithNoMediaAllowed, dataChannelOptions, this.isVoipWithNoMediaAllowed).create(); + } + + /** + * Wait until an initial state for the given room has been processed by the + * client and the client is aware of any ongoing group calls. Awaiting on + * the promise returned by this method before calling getGroupCallForRoom() + * avoids races where getGroupCallForRoom is called before the state for that + * room has been processed. It does not, however, fix other races, eg. two + * clients both creating a group call at the same time. + * @param roomId - The room ID to wait for + * @returns A promise that resolves once existing group calls in the room + * have been processed. */ + waitUntilRoomReadyForGroupCalls(roomId) { + return this.groupCallEventHandler.waitUntilRoomReadyForGroupCalls(roomId); + } + /** + * Get an existing group call for the provided room. + * @returns The group call or null if it doesn't already exist. + */ + getGroupCallForRoom(roomId) { + return this.groupCallEventHandler.groupCalls.get(roomId) || null; + } + /** + * Get the current sync state. + * @returns the sync state, which may be null. + * @see MatrixClient#event:"sync" + */ getSyncState() { - if (!this.syncApi) { - return null; - } - - return this.syncApi.getSyncState(); + return this.syncApi?.getSyncState() ?? null; } + /** * Returns the additional data object associated with * the current sync state, or null if there is no * such data. * Sync errors, if available, are put in the 'error' key of * this object. - * @return {?Object} */ - - getSyncStateData() { if (!this.syncApi) { return null; } - return this.syncApi.getSyncStateData(); } + /** * Whether the initial sync has completed. - * @return {boolean} True if at least one sync has happened. + * @returns True if at least one sync has happened. */ - - isInitialSyncComplete() { const state = this.getSyncState(); - if (!state) { return false; } - return state === _sync.SyncState.Prepared || state === _sync.SyncState.Syncing; } + /** * Return whether the client is configured for a guest account. - * @return {boolean} True if this is a guest access_token (or no token is supplied). + * @returns True if this is a guest access_token (or no token is supplied). */ - - isGuest() { return this.isGuestAccount; } + /** * Set whether this client is a guest account. This method is experimental * and may change without warning. - * @param {boolean} guest True if this is a guest account. + * @param guest - True if this is a guest account. */ - - setGuest(guest) { // EXPERIMENTAL: // If the token is a macaroon, it should be encoded in it that it is a 'guest' @@ -926,89 +877,81 @@ // the dev manually flipping this flag. this.isGuestAccount = guest; } + /** * Return the provided scheduler, if any. - * @return {?module:scheduler~MatrixScheduler} The scheduler or null + * @returns The scheduler or undefined */ - - getScheduler() { return this.scheduler; } + /** * Retry a backed off syncing request immediately. This should only be used when * the user explicitly attempts to retry their lost connection. * Will also retry any outbound to-device messages currently in the queue to be sent * (retries of regular outgoing events are handled separately, per-event). - * @return {boolean} True if this resulted in a request being retried. + * @returns True if this resulted in a request being retried. */ - - retryImmediately() { // don't await for this promise: we just want to kick it off this.toDeviceMessageQueue.sendQueue(); - return this.syncApi.retryImmediately(); + return this.syncApi?.retryImmediately() ?? false; } + /** * Return the global notification EventTimelineSet, if any * - * @return {EventTimelineSet} the globl notification EventTimelineSet + * @returns the globl notification EventTimelineSet */ - - getNotifTimelineSet() { return this.notifTimelineSet; } + /** * Set the global notification EventTimelineSet * - * @param {EventTimelineSet} set */ - - setNotifTimelineSet(set) { this.notifTimelineSet = set; } + /** * Gets the capabilities of the homeserver. Always returns an object of * capability keys and their options, which may be empty. - * @param {boolean} fresh True to ignore any cached values. - * @return {Promise} Resolves to the capabilities of the homeserver - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param fresh - True to ignore any cached values. + * @returns Promise which resolves to the capabilities of the homeserver + * @returns Rejects: with an error response. */ - - getCapabilities(fresh = false) { const now = new Date().getTime(); - if (this.cachedCapabilities && !fresh) { if (now < this.cachedCapabilities.expiration) { _logger.logger.log("Returning cached capabilities"); - return Promise.resolve(this.cachedCapabilities.capabilities); } } - - return this.http.authedRequest(undefined, _httpApi.Method.Get, "/capabilities").catch(e => { + return this.http.authedRequest(_httpApi.Method.Get, "/capabilities").catch(e => { // We swallow errors because we need a default object anyhow _logger.logger.error(e); + return {}; }).then((r = {}) => { - const capabilities = r["capabilities"] || {}; // If the capabilities missed the cache, cache it for a shorter amount - // of time to try and refresh them later. + const capabilities = r["capabilities"] || {}; + // If the capabilities missed the cache, cache it for a shorter amount + // of time to try and refresh them later. const cacheMs = Object.keys(capabilities).length ? CAPABILITIES_CACHE_MS : 60000 + Math.random() * 5000; this.cachedCapabilities = { capabilities, expiration: now + cacheMs }; - _logger.logger.log("Caching capabilities: ", capabilities); - return capabilities; }); } + /** - * Initialise support for end-to-end encryption in this client + * Initialise support for end-to-end encryption in this client, using libolm. * * You should call this method after creating the matrixclient, but *before* * calling `startClient`, if you want to support end-to-end encryption. @@ -1016,350 +959,348 @@ * It will return a Promise which will resolve when the crypto layer has been * successfully initialised. */ - - async initCrypto() { if (!(0, _crypto.isCryptoAvailable)()) { throw new Error(`End-to-end encryption not supported in this js-sdk build: did ` + `you remember to load the olm library?`); } - - if (this.crypto) { + if (this.cryptoBackend) { _logger.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); - return; } - if (!this.cryptoStore) { // the cryptostore is provided by sdk.createClient, so this shouldn't happen throw new Error(`Cannot enable encryption: no cryptoStore provided`); } - _logger.logger.log("Crypto: Starting up crypto store..."); + await this.cryptoStore.startup(); - await this.cryptoStore.startup(); // initialise the list of encrypted rooms (whether or not crypto is enabled) - + // initialise the list of encrypted rooms (whether or not crypto is enabled) _logger.logger.log("Crypto: initialising roomlist..."); - await this.roomList.init(); const userId = this.getUserId(); - if (userId === null) { throw new Error(`Cannot enable encryption on MatrixClient with unknown userId: ` + `ensure userId is passed in createClient().`); } - if (this.deviceId === null) { throw new Error(`Cannot enable encryption on MatrixClient with unknown deviceId: ` + `ensure deviceId is passed in createClient().`); } - const crypto = new _crypto.Crypto(this, userId, this.deviceId, this.store, this.cryptoStore, this.roomList, this.verificationMethods); this.reEmitter.reEmit(crypto, [_crypto.CryptoEvent.KeyBackupFailed, _crypto.CryptoEvent.KeyBackupSessionsRemaining, _crypto.CryptoEvent.RoomKeyRequest, _crypto.CryptoEvent.RoomKeyRequestCancellation, _crypto.CryptoEvent.Warning, _crypto.CryptoEvent.DevicesUpdated, _crypto.CryptoEvent.WillUpdateDevices, _crypto.CryptoEvent.DeviceVerificationChanged, _crypto.CryptoEvent.UserTrustStatusChanged, _crypto.CryptoEvent.KeysChanged]); - _logger.logger.log("Crypto: initialising crypto object..."); - await crypto.init({ exportedOlmDevice: this.exportedOlmDeviceToImport, pickleKey: this.pickleKey }); delete this.exportedOlmDeviceToImport; - this.olmVersion = _crypto.Crypto.getOlmVersion(); // if crypto initialisation was successful, tell it to attach its event handlers. + this.olmVersion = _crypto.Crypto.getOlmVersion(); + // if crypto initialisation was successful, tell it to attach its event handlers. crypto.registerEventHandlers(this); - this.crypto = crypto; + this.cryptoBackend = this.crypto = crypto; + + // upload our keys in the background + this.crypto.uploadDeviceKeys().catch(e => { + // TODO: throwing away this error is a really bad idea. + _logger.logger.error("Error uploading device keys", e); + }); } + /** - * Is end-to-end crypto enabled for this client. - * @return {boolean} True if end-to-end is enabled. + * Initialise support for end-to-end encryption in this client, using the rust matrix-sdk-crypto. + * + * An alternative to {@link initCrypto}. + * + * *WARNING*: this API is very experimental, should not be used in production, and may change without notice! + * Eventually it will be deprecated and `initCrypto` will do the same thing. + * + * @experimental + * + * @returns a Promise which will resolve when the crypto layer has been + * successfully initialised. */ + async initRustCrypto() { + if (this.cryptoBackend) { + _logger.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient"); + return; + } + const userId = this.getUserId(); + if (userId === null) { + throw new Error(`Cannot enable encryption on MatrixClient with unknown userId: ` + `ensure userId is passed in createClient().`); + } + const deviceId = this.getDeviceId(); + if (deviceId === null) { + throw new Error(`Cannot enable encryption on MatrixClient with unknown deviceId: ` + `ensure deviceId is passed in createClient().`); + } + + // importing rust-crypto will download the webassembly, so we delay it until we know it will be + // needed. + const RustCrypto = await Promise.resolve().then(() => _interopRequireWildcard(require("./rust-crypto"))); + const rustCrypto = await RustCrypto.initRustCrypto(this.http, userId, deviceId); + this.cryptoBackend = rustCrypto; + // attach the event listeners needed by RustCrypto + this.on(_roomMember.RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto)); + } + /** + * Is end-to-end crypto enabled for this client. + * @returns True if end-to-end is enabled. + */ isCryptoEnabled() { - return !!this.crypto; + return !!this.cryptoBackend; } + /** * Get the Ed25519 key for this device * - * @return {?string} base64-encoded ed25519 key. Null if crypto is + * @returns base64-encoded ed25519 key. Null if crypto is * disabled. */ - - getDeviceEd25519Key() { - if (!this.crypto) return null; - return this.crypto.getDeviceEd25519Key(); + return this.crypto?.getDeviceEd25519Key() ?? null; } + /** * Get the Curve25519 key for this device * - * @return {?string} base64-encoded curve25519 key. Null if crypto is + * @returns base64-encoded curve25519 key. Null if crypto is * disabled. */ - - getDeviceCurve25519Key() { - if (!this.crypto) return null; - return this.crypto.getDeviceCurve25519Key(); + return this.crypto?.getDeviceCurve25519Key() ?? null; } + /** - * Upload the device keys to the homeserver. - * @return {Promise} A promise that will resolve when the keys are uploaded. + * @deprecated Does nothing. */ - - async uploadKeys() { - if (!this.crypto) { - throw new Error("End-to-end encryption disabled"); - } - - await this.crypto.uploadDeviceKeys(); + _logger.logger.warn("MatrixClient.uploadKeys is deprecated"); } + /** * Download the keys for a list of users and stores the keys in the session * store. - * @param {Array} userIds The users to fetch. - * @param {boolean} forceDownload Always download the keys even if cached. + * @param userIds - The users to fetch. + * @param forceDownload - Always download the keys even if cached. * - * @return {Promise} A promise which resolves to a map userId->deviceId->{@link - * module:crypto~DeviceInfo|DeviceInfo}. + * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo} */ - - downloadKeys(userIds, forceDownload) { if (!this.crypto) { return Promise.reject(new Error("End-to-end encryption disabled")); } - return this.crypto.downloadKeys(userIds, forceDownload); } + /** * Get the stored device keys for a user id * - * @param {string} userId the user to list keys for. + * @param userId - the user to list keys for. * - * @return {module:crypto/deviceinfo[]} list of devices + * @returns list of devices */ - - getStoredDevicesForUser(userId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.getStoredDevicesForUser(userId) || []; } + /** * Get the stored device key for a user id and device id * - * @param {string} userId the user to list keys for. - * @param {string} deviceId unique identifier for the device + * @param userId - the user to list keys for. + * @param deviceId - unique identifier for the device * - * @return {module:crypto/deviceinfo} device or null + * @returns device or null */ - - getStoredDevice(userId, deviceId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.getStoredDevice(userId, deviceId) || null; } + /** * Mark the given device as verified * - * @param {string} userId owner of the device - * @param {string} deviceId unique identifier for the device or user's + * @param userId - owner of the device + * @param deviceId - unique identifier for the device or user's * cross-signing public key ID. * - * @param {boolean=} verified whether to mark the device as verified. defaults + * @param verified - whether to mark the device as verified. defaults * to 'true'. * - * @returns {Promise} + * @returns * - * @fires module:client~event:MatrixClient"deviceVerificationChanged" + * @remarks + * Fires {@link CryptoEvent#DeviceVerificationChanged} */ - - setDeviceVerified(userId, deviceId, verified = true) { - const prom = this.setDeviceVerification(userId, deviceId, verified, null, null); // if one of the user's own devices is being marked as verified / unverified, + const prom = this.setDeviceVerification(userId, deviceId, verified, null, null); + + // if one of the user's own devices is being marked as verified / unverified, // check the key backup status, since whether or not we use this depends on // whether it has a signature from a verified device - if (userId == this.credentials.userId) { this.checkKeyBackup(); } - return prom; } + /** * Mark the given device as blocked/unblocked * - * @param {string} userId owner of the device - * @param {string} deviceId unique identifier for the device or user's + * @param userId - owner of the device + * @param deviceId - unique identifier for the device or user's * cross-signing public key ID. * - * @param {boolean=} blocked whether to mark the device as blocked. defaults + * @param blocked - whether to mark the device as blocked. defaults * to 'true'. * - * @returns {Promise} + * @returns * - * @fires module:client~event:MatrixClient"deviceVerificationChanged" + * @remarks + * Fires {@link CryptoEvent.DeviceVerificationChanged} */ - - setDeviceBlocked(userId, deviceId, blocked = true) { return this.setDeviceVerification(userId, deviceId, null, blocked, null); } + /** * Mark the given device as known/unknown * - * @param {string} userId owner of the device - * @param {string} deviceId unique identifier for the device or user's + * @param userId - owner of the device + * @param deviceId - unique identifier for the device or user's * cross-signing public key ID. * - * @param {boolean=} known whether to mark the device as known. defaults + * @param known - whether to mark the device as known. defaults * to 'true'. * - * @returns {Promise} + * @returns * - * @fires module:client~event:MatrixClient"deviceVerificationChanged" + * @remarks + * Fires {@link CryptoEvent#DeviceVerificationChanged} */ - - setDeviceKnown(userId, deviceId, known = true) { return this.setDeviceVerification(userId, deviceId, null, null, known); } - async setDeviceVerification(userId, deviceId, verified, blocked, known) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - await this.crypto.setDeviceVerification(userId, deviceId, verified, blocked, known); } + /** * Request a key verification from another user, using a DM. * - * @param {string} userId the user to request verification with - * @param {string} roomId the room to use for verification + * @param userId - the user to request verification with + * @param roomId - the room to use for verification * - * @returns {Promise} resolves to a VerificationRequest + * @returns resolves to a VerificationRequest * when the request has been sent to the other party. */ - - requestVerificationDM(userId, roomId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.requestVerificationDM(userId, roomId); } + /** * Finds a DM verification request that is already in progress for the given room id * - * @param {string} roomId the room to use for verification + * @param roomId - the room to use for verification * - * @returns {module:crypto/verification/request/VerificationRequest?} the VerificationRequest that is in progress, if any + * @returns the VerificationRequest that is in progress, if any */ - - findVerificationRequestDMInProgress(roomId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.findVerificationRequestDMInProgress(roomId); } + /** * Returns all to-device verification requests that are already in progress for the given user id * - * @param {string} userId the ID of the user to query + * @param userId - the ID of the user to query * - * @returns {module:crypto/verification/request/VerificationRequest[]} the VerificationRequests that are in progress + * @returns the VerificationRequests that are in progress */ - - getVerificationRequestsToDeviceInProgress(userId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.getVerificationRequestsToDeviceInProgress(userId); } + /** * Request a key verification from another user. * - * @param {string} userId the user to request verification with - * @param {Array} devices array of device IDs to send requests to. Defaults to + * @param userId - the user to request verification with + * @param devices - array of device IDs to send requests to. Defaults to * all devices owned by the user * - * @returns {Promise} resolves to a VerificationRequest + * @returns resolves to a VerificationRequest * when the request has been sent to the other party. */ - - requestVerification(userId, devices) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.requestVerification(userId, devices); } + /** * Begin a key verification. * - * @param {string} method the verification method to use - * @param {string} userId the user to verify keys with - * @param {string} deviceId the device to verify + * @param method - the verification method to use + * @param userId - the user to verify keys with + * @param deviceId - the device to verify * - * @returns {Verification} a verification object + * @returns a verification object * @deprecated Use `requestVerification` instead. */ - - beginKeyVerification(method, userId, deviceId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.beginKeyVerification(method, userId, deviceId); } - checkSecretStorageKey(key, info) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.checkSecretStorageKey(key, info); } + /** * Set the global override for whether the client should ever send encrypted * messages to unverified devices. This provides the default for rooms which * do not specify a value. * - * @param {boolean} value whether to blacklist all unverified devices by default + * @param value - whether to blacklist all unverified devices by default */ - - setGlobalBlacklistUnverifiedDevices(value) { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - - return this.crypto.setGlobalBlacklistUnverifiedDevices(value); + this.cryptoBackend.globalBlacklistUnverifiedDevices = value; + return value; } + /** - * @return {boolean} whether to blacklist all unverified devices by default + * @returns whether to blacklist all unverified devices by default */ - - getGlobalBlacklistUnverifiedDevices() { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - - return this.crypto.getGlobalBlacklistUnverifiedDevices(); + return this.cryptoBackend.globalBlacklistUnverifiedDevices; } + /** * Set whether sendMessage in a room with unknown and unverified devices * should throw an error and not send them message. This has 'Global' for @@ -1368,177 +1309,166 @@ * * This API is currently UNSTABLE and may change or be removed without notice. * - * @param {boolean} value whether error on unknown devices + * @param value - whether error on unknown devices */ - - setGlobalErrorOnUnknownDevices(value) { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - - return this.crypto.setGlobalErrorOnUnknownDevices(value); + this.cryptoBackend.globalErrorOnUnknownDevices = value; } + /** - * @return {boolean} whether to error on unknown devices + * @returns whether to error on unknown devices * * This API is currently UNSTABLE and may change or be removed without notice. */ - - getGlobalErrorOnUnknownDevices() { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - - return this.crypto.getGlobalErrorOnUnknownDevices(); + return this.cryptoBackend.globalErrorOnUnknownDevices; } + /** * Get the user's cross-signing key ID. * * The cross-signing API is currently UNSTABLE and may change without notice. * - * @param {CrossSigningKey} [type=master] The type of key to get the ID of. One of + * @param type - The type of key to get the ID of. One of * "master", "self_signing", or "user_signing". Defaults to "master". * - * @returns {string} the key ID + * @returns the key ID */ - - getCrossSigningId(type = _api.CrossSigningKey.Master) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.getCrossSigningId(type); } + /** * Get the cross signing information for a given user. * * The cross-signing API is currently UNSTABLE and may change without notice. * - * @param {string} userId the user ID to get the cross-signing info for. + * @param userId - the user ID to get the cross-signing info for. * - * @returns {CrossSigningInfo} the cross signing information for the user. + * @returns the cross signing information for the user. */ - - getStoredCrossSigningForUser(userId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.getStoredCrossSigningForUser(userId); } + /** * Check whether a given user is trusted. * * The cross-signing API is currently UNSTABLE and may change without notice. * - * @param {string} userId The ID of the user to check. + * @param userId - The ID of the user to check. * - * @returns {UserTrustLevel} + * @returns */ - - checkUserTrust(userId) { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - - return this.crypto.checkUserTrust(userId); + return this.cryptoBackend.checkUserTrust(userId); } + /** * Check whether a given device is trusted. * * The cross-signing API is currently UNSTABLE and may change without notice. * - * @function module:client~MatrixClient#checkDeviceTrust - * @param {string} userId The ID of the user whose devices is to be checked. - * @param {string} deviceId The ID of the device to check - * - * @returns {DeviceTrustLevel} + * @param userId - The ID of the user whose devices is to be checked. + * @param deviceId - The ID of the device to check */ - - checkDeviceTrust(userId, deviceId) { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - - return this.crypto.checkDeviceTrust(userId, deviceId); + return this.cryptoBackend.checkDeviceTrust(userId, deviceId); } + /** * Check whether one of our own devices is cross-signed by our * user's stored keys, regardless of whether we trust those keys yet. * - * @param {string} deviceId The ID of the device to check + * @param deviceId - The ID of the device to check * - * @returns {boolean} true if the device is cross-signed + * @returns true if the device is cross-signed */ - - checkIfOwnDeviceCrossSigned(deviceId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.checkIfOwnDeviceCrossSigned(deviceId); } + /** * Check the copy of our cross-signing key that we have in the device list and * see if we can get the private key. If so, mark it as trusted. - * @param {Object} opts ICheckOwnCrossSigningTrustOpts object + * @param opts - ICheckOwnCrossSigningTrustOpts object */ - - checkOwnCrossSigningTrust(opts) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.checkOwnCrossSigningTrust(opts); } + /** * Checks that a given cross-signing private key matches a given public key. * This can be used by the getCrossSigningKey callback to verify that the * private key it is about to supply is the one that was requested. - * @param {Uint8Array} privateKey The private key - * @param {string} expectedPublicKey The public key - * @returns {boolean} true if the key matches, otherwise false + * @param privateKey - The private key + * @param expectedPublicKey - The public key + * @returns true if the key matches, otherwise false */ - - checkCrossSigningPrivateKey(privateKey, expectedPublicKey) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.checkCrossSigningPrivateKey(privateKey, expectedPublicKey); - } // deprecated: use requestVerification instead - + } + // deprecated: use requestVerification instead legacyDeviceVerification(userId, deviceId, method) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.legacyDeviceVerification(userId, deviceId, method); } + /** * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. - * @param {module:models/room} room the room the event is in + * @param room - the room the event is in */ - - prepareToEncrypt(room) { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } + this.cryptoBackend.prepareToEncrypt(room); + } - return this.crypto.prepareToEncrypt(room); + /** + * Checks if the user has previously published cross-signing keys + * + * This means downloading the devicelist for the user and checking if the list includes + * the cross-signing pseudo-device. + */ + userHasCrossSigningKeys() { + if (!this.cryptoBackend) { + throw new Error("End-to-end encryption disabled"); + } + return this.cryptoBackend.userHasCrossSigningKeys(); } + /** * Checks whether cross signing: * - is enabled on this account and trusted by this device @@ -1548,17 +1478,15 @@ * to fix things such that it returns true. That is to say, after * bootstrapCrossSigning() completes successfully, this function should * return true. - * @return {boolean} True if cross-signing is ready to be used on this device + * @returns True if cross-signing is ready to be used on this device */ - - isCrossSigningReady() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.isCrossSigningReady(); } + /** * Bootstrap cross-signing by creating keys if needed. If everything is already * set up, then no changes are made, so this is safe to run to ensure @@ -1569,25 +1497,14 @@ * secret storage (if it has been setup) * * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @param {function} opts.authUploadDeviceSigningKeys Function - * called to await an interactive auth flow when uploading device signing keys. - * @param {boolean} [opts.setupNewCrossSigning] Optional. Reset even if keys - * already exist. - * Args: - * {function} A function that makes the request requiring auth. Receives the - * auth data as an object. Can be called multiple times, first with an empty - * authDict, to obtain the flows. */ - - bootstrapCrossSigning(opts) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.bootstrapCrossSigning(opts); } + /** * Whether to trust a others users signatures of their devices. * If false, devices will only be considered 'verified' if we have @@ -1595,81 +1512,70 @@ * * Default: true * - * @return {boolean} True if trusting cross-signed devices + * @returns True if trusting cross-signed devices */ - - getCryptoTrustCrossSignedDevices() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.getCryptoTrustCrossSignedDevices(); } + /** * See getCryptoTrustCrossSignedDevices - * This may be set before initCrypto() is called to ensure no races occur. * - * @param {boolean} val True to trust cross-signed devices + * @param val - True to trust cross-signed devices */ - - setCryptoTrustCrossSignedDevices(val) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - - return this.crypto.setCryptoTrustCrossSignedDevices(val); + this.crypto.setCryptoTrustCrossSignedDevices(val); } + /** * Counts the number of end to end session keys that are waiting to be backed up - * @returns {Promise} Resolves to the number of sessions requiring backup + * @returns Promise which resolves to the number of sessions requiring backup */ - - countSessionsNeedingBackup() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.countSessionsNeedingBackup(); } + /** * Get information about the encryption of an event * - * @param {module:models/event.MatrixEvent} event event to be checked - * @returns {IEncryptedEventInfo} The event information. + * @param event - event to be checked + * @returns The event information. */ - - getEventEncryptionInfo(event) { - if (!this.crypto) { + if (!this.cryptoBackend) { throw new Error("End-to-end encryption disabled"); } - - return this.crypto.getEventEncryptionInfo(event); + return this.cryptoBackend.getEventEncryptionInfo(event); } + /** * Create a recovery key from a user-supplied passphrase. * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {string} password Passphrase string that can be entered by the user + * @param password - Passphrase string that can be entered by the user * when restoring the backup as an alternative to entering the recovery key. * Optional. - * @returns {Promise} Object with public key metadata, encoded private + * @returns Object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. */ - - createRecoveryKeyFromPassphrase(password) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.createRecoveryKeyFromPassphrase(password); } + /** * Checks whether secret storage: * - is enabled on this account @@ -1683,17 +1589,15 @@ * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @return {boolean} True if secret storage is ready to be used on this device + * @returns True if secret storage is ready to be used on this device */ - - isSecretStorageReady() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.isSecretStorageReady(); } + /** * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is * already set up, then no changes are made, so this is safe to run to ensure secret @@ -1707,165 +1611,146 @@ * - migrates Secure Secret Storage to use the latest algorithm, if an outdated * algorithm is found * - * @param opts */ - - bootstrapSecretStorage(opts) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.bootstrapSecretStorage(opts); } + /** * Add a key for encrypting secrets. * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {string} algorithm the algorithm used by the key - * @param {object} opts the options for the algorithm. The properties used + * @param algorithm - the algorithm used by the key + * @param opts - the options for the algorithm. The properties used * depend on the algorithm given. - * @param {string} [keyName] the name of the key. If not given, a random name will be generated. + * @param keyName - the name of the key. If not given, a random name will be generated. * - * @return {object} An object with: - * keyId: {string} the ID of the key - * keyInfo: {object} details about the key (iv, mac, passphrase) + * @returns An object with: + * keyId: the ID of the key + * keyInfo: details about the key (iv, mac, passphrase) */ - - addSecretStorageKey(algorithm, opts, keyName) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.addSecretStorageKey(algorithm, opts, keyName); } + /** * Check whether we have a key with a given ID. * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {string} [keyId = default key's ID] The ID of the key to check + * @param keyId - The ID of the key to check * for. Defaults to the default key ID if not provided. - * @return {boolean} Whether we have the key. + * @returns Whether we have the key. */ - - hasSecretStorageKey(keyId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.hasSecretStorageKey(keyId); } + /** * Store an encrypted secret on the server. * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {string} name The name of the secret - * @param {string} secret The secret contents. - * @param {Array} keys The IDs of the keys to use to encrypt the secret or null/undefined + * @param name - The name of the secret + * @param secret - The secret contents. + * @param keys - The IDs of the keys to use to encrypt the secret or null/undefined * to use the default (will throw if no default key is set). */ - - storeSecret(name, secret, keys) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.storeSecret(name, secret, keys); } + /** * Get a secret from storage. * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {string} name the name of the secret + * @param name - the name of the secret * - * @return {string} the contents of the secret + * @returns the contents of the secret */ - - getSecret(name) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.getSecret(name); } + /** * Check if a secret is stored on the server. * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {string} name the name of the secret - * @return {object?} map of key name to key info the secret is encrypted + * @param name - the name of the secret + * @returns map of key name to key info the secret is encrypted * with, or null if it is not present or not encrypted with a trusted * key */ - - isSecretStored(name) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.isSecretStored(name); } + /** * Request a secret from another device. * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {string} name the name of the secret to request - * @param {string[]} devices the devices to request the secret from + * @param name - the name of the secret to request + * @param devices - the devices to request the secret from * - * @return {ISecretRequest} the secret request object + * @returns the secret request object */ - - requestSecret(name, devices) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.requestSecret(name, devices); } + /** * Get the current default key ID for encrypting secrets. * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @return {string} The default key ID or null if no default key ID is set + * @returns The default key ID or null if no default key ID is set */ - - getDefaultSecretStorageKeyId() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.getDefaultSecretStorageKeyId(); } + /** * Set the current default key ID for encrypting secrets. * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {string} keyId The new default key ID + * @param keyId - The new default key ID */ - - setDefaultSecretStorageKeyId(keyId) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.setDefaultSecretStorageKeyId(keyId); } + /** * Checks that a given secret storage private key matches a given public key. * This can be used by the getSecretStorageKey callback to verify that the @@ -1873,368 +1758,341 @@ * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {Uint8Array} privateKey The private key - * @param {string} expectedPublicKey The public key - * @returns {boolean} true if the key matches, otherwise false + * @param privateKey - The private key + * @param expectedPublicKey - The public key + * @returns true if the key matches, otherwise false */ - - checkSecretStoragePrivateKey(privateKey, expectedPublicKey) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.checkSecretStoragePrivateKey(privateKey, expectedPublicKey); } + /** * Get e2e information on the device that sent an event * - * @param {MatrixEvent} event event to be checked - * - * @return {Promise} + * @param event - event to be checked */ - - async getEventSenderDeviceInfo(event) { if (!this.crypto) { return null; } - return this.crypto.getEventSenderDeviceInfo(event); } + /** * Check if the sender of an event is verified * - * @param {MatrixEvent} event event to be checked + * @param event - event to be checked * - * @return {boolean} true if the sender of this event has been verified using - * {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}. + * @returns true if the sender of this event has been verified using + * {@link MatrixClient#setDeviceVerified}. */ - - async isEventSenderVerified(event) { const device = await this.getEventSenderDeviceInfo(event); - if (!device) { return false; } - return device.isVerified(); } + + /** + * Get outgoing room key request for this event if there is one. + * @param event - The event to check for + * + * @returns A room key request, or null if there is none + */ + getOutgoingRoomKeyRequest(event) { + if (!this.crypto) { + throw new Error("End-to-End encryption disabled"); + } + const wireContent = event.getWireContent(); + const requestBody = { + session_id: wireContent.session_id, + sender_key: wireContent.sender_key, + algorithm: wireContent.algorithm, + room_id: event.getRoomId() + }; + if (!requestBody.session_id || !requestBody.sender_key || !requestBody.algorithm || !requestBody.room_id) { + return Promise.resolve(null); + } + return this.crypto.cryptoStore.getOutgoingRoomKeyRequest(requestBody); + } + /** * Cancel a room key request for this event if one is ongoing and resend the * request. - * @param {MatrixEvent} event event of which to cancel and resend the room + * @param event - event of which to cancel and resend the room * key request. - * @return {Promise} A promise that will resolve when the key request is queued + * @returns A promise that will resolve when the key request is queued */ - - cancelAndResendEventRoomKeyRequest(event) { + if (!this.crypto) { + throw new Error("End-to-End encryption disabled"); + } return event.cancelAndResendKeyRequest(this.crypto, this.getUserId()); } + /** * Enable end-to-end encryption for a room. This does not modify room state. * Any messages sent before the returned promise resolves will be sent unencrypted. - * @param {string} roomId The room ID to enable encryption in. - * @param {object} config The encryption config for the room. - * @return {Promise} A promise that will resolve when encryption is set up. + * @param roomId - The room ID to enable encryption in. + * @param config - The encryption config for the room. + * @returns A promise that will resolve when encryption is set up. */ - - setRoomEncryption(roomId, config) { if (!this.crypto) { throw new Error("End-to-End encryption disabled"); } - return this.crypto.setRoomEncryption(roomId, config); } + /** * Whether encryption is enabled for a room. - * @param {string} roomId the room id to query. - * @return {boolean} whether encryption is enabled. + * @param roomId - the room id to query. + * @returns whether encryption is enabled. */ - - isRoomEncrypted(roomId) { const room = this.getRoom(roomId); - if (!room) { // we don't know about this room, so can't determine if it should be // encrypted. Let's assume not. return false; - } // if there is an 'm.room.encryption' event in this room, it should be - // encrypted (independently of whether we actually support encryption) - + } + // if there is an 'm.room.encryption' event in this room, it should be + // encrypted (independently of whether we actually support encryption) const ev = room.currentState.getStateEvents(_event2.EventType.RoomEncryption, ""); - if (ev) { return true; - } // we don't have an m.room.encrypted event, but that might be because + } + + // we don't have an m.room.encrypted event, but that might be because // the server is hiding it from us. Check the store to see if it was // previously encrypted. - - return this.roomList.isRoomEncrypted(roomId); } + /** * Encrypts and sends a given object via Olm to-device messages to a given * set of devices. * - * @param {object[]} userDeviceInfoArr - * mapping from userId to deviceInfo + * @param userDeviceMap - mapping from userId to deviceInfo + * + * @param payload - fields to include in the encrypted payload * - * @param {object} payload fields to include in the encrypted payload - * * - * @return {Promise<{contentMap, deviceInfoByDeviceId}>} Promise which + * @returns Promise which * resolves once the message has been encrypted and sent to the given - * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }` * of the successfully sent messages. */ - - encryptAndSendToDevices(userDeviceInfoArr, payload) { if (!this.crypto) { throw new Error("End-to-End encryption disabled"); } - return this.crypto.encryptAndSendToDevices(userDeviceInfoArr, payload); } + /** * Forces the current outbound group session to be discarded such * that another one will be created next time an event is sent. * - * @param {string} roomId The ID of the room to discard the session for + * @param roomId - The ID of the room to discard the session for * * This should not normally be necessary. */ - - forceDiscardSession(roomId) { if (!this.crypto) { throw new Error("End-to-End encryption disabled"); } - this.crypto.forceDiscardSession(roomId); } + /** * Get a list containing all of the room keys * * This should be encrypted before returning it to the user. * - * @return {Promise} a promise which resolves to a list of + * @returns a promise which resolves to a list of * session export objects */ - - exportRoomKeys() { - if (!this.crypto) { + if (!this.cryptoBackend) { return Promise.reject(new Error("End-to-end encryption disabled")); } - - return this.crypto.exportRoomKeys(); + return this.cryptoBackend.exportRoomKeys(); } + /** * Import a list of room keys previously exported by exportRoomKeys * - * @param {Object[]} keys a list of session export objects - * @param {Object} opts - * @param {Function} opts.progressCallback called with an object that has a "stage" param + * @param keys - a list of session export objects * - * @return {Promise} a promise which resolves when the keys - * have been imported + * @returns a promise which resolves when the keys have been imported */ - - importRoomKeys(keys, opts) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.importRoomKeys(keys, opts); } + /** * Force a re-check of the local key backup status against * what's on the server. * - * @returns {Object} Object with backup info (as returned by + * @returns Object with backup info (as returned by * getKeyBackupVersion) in backupInfo and * trust information (as returned by isKeyBackupTrusted) * in trustInfo. */ - - checkKeyBackup() { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } return this.crypto.backupManager.checkKeyBackup(); } + /** * Get information about the current key backup. - * @returns {Promise} Information object from API or null + * @returns Information object from API or null */ - - async getKeyBackupVersion() { let res; - try { - res = await this.http.authedRequest(undefined, _httpApi.Method.Get, "/room_keys/version", undefined, undefined, { - prefix: _httpApi.PREFIX_UNSTABLE + res = await this.http.authedRequest(_httpApi.Method.Get, "/room_keys/version", undefined, undefined, { + prefix: _httpApi.ClientPrefix.V3 }); } catch (e) { - if (e.errcode === 'M_NOT_FOUND') { + if (e.errcode === "M_NOT_FOUND") { return null; } else { throw e; } } - _backup.BackupManager.checkBackupVersion(res); - return res; } + /** - * @param {object} info key backup info dict from getKeyBackupVersion() - * @return {object} { - * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device - * sigs: [ - * valid: [bool], - * device: [DeviceInfo], - * ] - * } + * @param info - key backup info dict from getKeyBackupVersion() */ - - isKeyBackupTrusted(info) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } return this.crypto.backupManager.isKeyBackupTrusted(info); } + /** - * @returns {boolean} true if the client is configured to back up keys to + * @returns true if the client is configured to back up keys to * the server, otherwise false. If we haven't completed a successful check * of key backup status yet, returns null. */ - - getKeyBackupEnabled() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.backupManager.getKeyBackupEnabled(); } + /** * Enable backing up of keys, using data previously returned from * getKeyBackupVersion. * - * @param {object} info Backup information object as returned by getKeyBackupVersion - * @returns {Promise} Resolves when complete. + * @param info - Backup information object as returned by getKeyBackupVersion + * @returns Promise which resolves when complete. */ - - enableKeyBackup(info) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.backupManager.enableKeyBackup(info); } + /** * Disable backing up of keys. */ - - disableKeyBackup() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - this.crypto.backupManager.disableKeyBackup(); } + /** * Set up the data required to create a new backup version. The backup version * will not be created and enabled until createKeyBackupVersion is called. * - * @param {string} password Passphrase string that can be entered by the user + * @param password - Passphrase string that can be entered by the user * when restoring the backup as an alternative to entering the recovery key. * Optional. - * @param {boolean} [opts.secureSecretStorage = false] Whether to use Secure - * Secret Storage to store the key encrypting key backups. - * Optional, defaults to false. * - * @returns {Promise} Object that can be passed to createKeyBackupVersion and + * @returns Object that can be passed to createKeyBackupVersion and * additionally has a 'recovery_key' member with the user-facing recovery key string. */ - // TODO: Verify types - - async prepareKeyBackupVersion(password, opts = { secureSecretStorage: false }) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); - } // eslint-disable-next-line camelcase - + } + // eslint-disable-next-line camelcase const { algorithm, auth_data, recovery_key, privateKey } = await this.crypto.backupManager.prepareKeyBackupVersion(password); - if (opts.secureSecretStorage) { await this.storeSecret("m.megolm_backup.v1", (0, olmlib.encodeBase64)(privateKey)); - _logger.logger.info("Key backup private key stored in secret storage"); } - return { algorithm, - /* eslint-disable camelcase */ auth_data, recovery_key /* eslint-enable camelcase */ - }; } + /** * Check whether the key backup private key is stored in secret storage. - * @return {Promise} map of key name to key info the secret is + * @returns map of key name to key info the secret is * encrypted with, or null if it is not present or not encrypted with a * trusted key */ - - isKeyBackupKeyStored() { return Promise.resolve(this.isSecretStored("m.megolm_backup.v1")); } + /** * Create a new key backup version and enable it, using the information return * from prepareKeyBackupVersion. * - * @param {object} info Info object from prepareKeyBackupVersion - * @returns {Promise} Object with 'version' param indicating the version created + * @param info - Info object from prepareKeyBackupVersion + * @returns Object with 'version' param indicating the version created */ - - async createKeyBackupVersion(info) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - await this.crypto.backupManager.createKeyBackupVersion(info); const data = { algorithm: info.algorithm, auth_data: info.auth_data - }; // Sign the backup auth data with the device key for backwards compat with + }; + + // Sign the backup auth data with the device key for backwards compat with // older devices with cross-signing. This can probably go away very soon in // favour of just signing with the cross-singing master key. // XXX: Private member access - await this.crypto.signObject(data.auth_data); - - if (this.cryptoCallbacks.getCrossSigningKey && // XXX: Private member access + if (this.cryptoCallbacks.getCrossSigningKey && + // XXX: Private member access this.crypto.crossSigningInfo.getId()) { // now also sign the auth data with the cross-signing master key // we check for the callback explicitly here because we still want to be able @@ -2243,45 +2101,39 @@ // XXX: Private member access await this.crypto.crossSigningInfo.signObject(data.auth_data, "master"); } + const res = await this.http.authedRequest(_httpApi.Method.Post, "/room_keys/version", undefined, data, { + prefix: _httpApi.ClientPrefix.V3 + }); - const res = await this.http.authedRequest(undefined, _httpApi.Method.Post, "/room_keys/version", undefined, data, { - prefix: _httpApi.PREFIX_UNSTABLE - }); // We could assume everything's okay and enable directly, but this ensures + // We could assume everything's okay and enable directly, but this ensures // we run the same signature verification that will be used for future // sessions. - await this.checkKeyBackup(); - if (!this.getKeyBackupEnabled()) { _logger.logger.error("Key backup not usable even though we just created it"); } - return res; } - - deleteKeyBackupVersion(version) { + async deleteKeyBackupVersion(version) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); - } // If we're currently backing up to this backup... stop. + } + + // If we're currently backing up to this backup... stop. // (We start using it automatically in createKeyBackupVersion // so this is symmetrical). - - if (this.crypto.backupManager.version) { this.crypto.backupManager.disableKeyBackup(); } - const path = utils.encodeUri("/room_keys/version/$version", { $version: version }); - return this.http.authedRequest(undefined, _httpApi.Method.Delete, path, undefined, undefined, { - prefix: _httpApi.PREFIX_UNSTABLE + await this.http.authedRequest(_httpApi.Method.Delete, path, undefined, undefined, { + prefix: _httpApi.ClientPrefix.V3 }); } - makeKeyBackupPath(roomId, sessionId, version) { let path; - if (sessionId !== undefined) { path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { $roomId: roomId, @@ -2294,7 +2146,6 @@ } else { path = "/room_keys/keys"; } - const queryData = version === undefined ? undefined : { version }; @@ -2303,55 +2154,49 @@ queryData }; } + /** * Back up session keys to the homeserver. - * @param {string} roomId ID of the room that the keys are for Optional. - * @param {string} sessionId ID of the session that the keys are for Optional. - * @param {number} version backup version Optional. - * @param {object} data Object keys to send - * @return {Promise} a promise that will resolve when the keys + * @param roomId - ID of the room that the keys are for Optional. + * @param sessionId - ID of the session that the keys are for Optional. + * @param version - backup version Optional. + * @param data - Object keys to send + * @returns a promise that will resolve when the keys * are uploaded */ - - sendKeyBackup(roomId, sessionId, version, data) { + async sendKeyBackup(roomId, sessionId, version, data) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - const path = this.makeKeyBackupPath(roomId, sessionId, version); - return this.http.authedRequest(undefined, _httpApi.Method.Put, path.path, path.queryData, data, { - prefix: _httpApi.PREFIX_UNSTABLE + await this.http.authedRequest(_httpApi.Method.Put, path.path, path.queryData, data, { + prefix: _httpApi.ClientPrefix.V3 }); } + /** * Marks all group sessions as needing to be backed up and schedules them to * upload in the background as soon as possible. */ - - async scheduleAllGroupSessionsForBackup() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - await this.crypto.backupManager.scheduleAllGroupSessionsForBackup(); } + /** * Marks all group sessions as needing to be backed up without scheduling * them to upload in the background. - * @returns {Promise} Resolves to the number of sessions requiring a backup. + * @returns Promise which resolves to the number of sessions requiring a backup. */ - - flagAllGroupSessionsForBackup() { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - return this.crypto.backupManager.flagAllGroupSessionsForBackup(); } - isValidRecoveryKey(recoveryKey) { try { (0, _recoverykey.decodeRecoveryKey)(recoveryKey); @@ -2360,121 +2205,117 @@ return false; } } + /** * Get the raw key for a key backup from the password * Used when migrating key backups into SSSS * * The cross-signing API is currently UNSTABLE and may change without notice. * - * @param {string} password Passphrase - * @param {object} backupInfo Backup metadata from `checkKeyBackup` - * @return {Promise} key backup key + * @param password - Passphrase + * @param backupInfo - Backup metadata from `checkKeyBackup` + * @returns key backup key */ - - keyBackupKeyFromPassword(password, backupInfo) { return (0, _key_passphrase.keyFromAuthData)(backupInfo.auth_data, password); } + /** * Get the raw key for a key backup from the recovery key * Used when migrating key backups into SSSS * * The cross-signing API is currently UNSTABLE and may change without notice. * - * @param {string} recoveryKey The recovery key - * @return {Uint8Array} key backup key + * @param recoveryKey - The recovery key + * @returns key backup key */ - - keyBackupKeyFromRecoveryKey(recoveryKey) { return (0, _recoverykey.decodeRecoveryKey)(recoveryKey); } + /** * Restore from an existing key backup via a passphrase. * - * @param {string} password Passphrase - * @param {string} [targetRoomId] Room ID to target a specific room. + * @param password - Passphrase + * @param targetRoomId - Room ID to target a specific room. * Restores all rooms if omitted. - * @param {string} [targetSessionId] Session ID to target a specific session. + * @param targetSessionId - Session ID to target a specific session. * Restores all sessions if omitted. - * @param {object} backupInfo Backup metadata from `checkKeyBackup` - * @param {object} opts Optional params such as callbacks - * @return {Promise} Status of restoration with `total` and `imported` + * @param backupInfo - Backup metadata from `checkKeyBackup` + * @param opts - Optional params such as callbacks + * @returns Status of restoration with `total` and `imported` * key counts. */ - async restoreKeyBackupWithPassword(password, targetRoomId, targetSessionId, backupInfo, opts) { const privKey = await (0, _key_passphrase.keyFromAuthData)(backupInfo.auth_data, password); return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); } + /** * Restore from an existing key backup via a private key stored in secret * storage. * - * @param {object} backupInfo Backup metadata from `checkKeyBackup` - * @param {string} [targetRoomId] Room ID to target a specific room. + * @param backupInfo - Backup metadata from `checkKeyBackup` + * @param targetRoomId - Room ID to target a specific room. * Restores all rooms if omitted. - * @param {string} [targetSessionId] Session ID to target a specific session. + * @param targetSessionId - Session ID to target a specific session. * Restores all sessions if omitted. - * @param {object} opts Optional params such as callbacks - * @return {Promise} Status of restoration with `total` and `imported` + * @param opts - Optional params such as callbacks + * @returns Status of restoration with `total` and `imported` * key counts. */ - - async restoreKeyBackupWithSecretStorage(backupInfo, targetRoomId, targetSessionId, opts) { - const storedKey = await this.getSecret("m.megolm_backup.v1"); // ensure that the key is in the right format. If not, fix the key and - // store the fixed version + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } + const storedKey = await this.getSecret("m.megolm_backup.v1"); + // ensure that the key is in the right format. If not, fix the key and + // store the fixed version const fixedKey = (0, _crypto.fixBackupKey)(storedKey); - if (fixedKey) { - const [keyId] = await this.crypto.getSecretStorageKey(); - await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); + const keys = await this.crypto.getSecretStorageKey(); + await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys[0]]); } - const privKey = (0, olmlib.decodeBase64)(fixedKey || storedKey); return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); } + /** * Restore from an existing key backup via an encoded recovery key. * - * @param {string} recoveryKey Encoded recovery key - * @param {string} [targetRoomId] Room ID to target a specific room. + * @param recoveryKey - Encoded recovery key + * @param targetRoomId - Room ID to target a specific room. * Restores all rooms if omitted. - * @param {string} [targetSessionId] Session ID to target a specific session. + * @param targetSessionId - Session ID to target a specific session. * Restores all sessions if omitted. - * @param {object} backupInfo Backup metadata from `checkKeyBackup` - * @param {object} opts Optional params such as callbacks - * @return {Promise} Status of restoration with `total` and `imported` + * @param backupInfo - Backup metadata from `checkKeyBackup` + * @param opts - Optional params such as callbacks + * @returns Status of restoration with `total` and `imported` * key counts. */ - restoreKeyBackupWithRecoveryKey(recoveryKey, targetRoomId, targetSessionId, backupInfo, opts) { const privKey = (0, _recoverykey.decodeRecoveryKey)(recoveryKey); return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); } - async restoreKeyBackupWithCache(targetRoomId, targetSessionId, backupInfo, opts) { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } const privKey = await this.crypto.getSessionBackupPrivateKey(); - if (!privKey) { throw new Error("Couldn't get key"); } - return this.restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts); } - async restoreKeyBackup(privKey, targetRoomId, targetSessionId, backupInfo, opts) { const cacheCompleteCallback = opts?.cacheCompleteCallback; const progressCallback = opts?.progressCallback; - if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - let totalKeyCount = 0; let keys = []; const path = this.makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version); @@ -2482,7 +2323,6 @@ return privKey; }); const untrusted = algorithm.untrusted; - try { // If the pubkey computed from the private data we've been given // doesn't match the one in the auth_data, the user has entered @@ -2491,32 +2331,27 @@ return Promise.reject(new _httpApi.MatrixError({ errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY })); - } // Cache the key, if possible. - // This is async. - + } + // Cache the key, if possible. + // This is async. this.crypto.storeSessionBackupPrivateKey(privKey).catch(e => { _logger.logger.warn("Error caching session backup key:", e); }).then(cacheCompleteCallback); - if (progressCallback) { progressCallback({ stage: "fetch" }); } - - const res = await this.http.authedRequest(undefined, _httpApi.Method.Get, path.path, path.queryData, undefined, { - prefix: _httpApi.PREFIX_UNSTABLE + const res = await this.http.authedRequest(_httpApi.Method.Get, path.path, path.queryData, undefined, { + prefix: _httpApi.ClientPrefix.V3 }); - if (res.rooms) { const rooms = res.rooms; - for (const [roomId, roomData] of Object.entries(rooms)) { if (!roomData.sessions) continue; totalKeyCount += Object.keys(roomData.sessions).length; const roomKeys = await algorithm.decryptSessions(roomData.sessions); - for (const k of roomKeys) { k.room_id = roomId; keys.push(k); @@ -2526,13 +2361,11 @@ const sessions = res.sessions; totalKeyCount = Object.keys(sessions).length; keys = await algorithm.decryptSessions(sessions); - for (const k of keys) { k.room_id = targetRoomId; } } else { totalKeyCount = 1; - try { const [key] = await algorithm.decryptSessions({ [targetSessionId]: res @@ -2547,7 +2380,6 @@ } finally { algorithm.free(); } - await this.importRoomKeys(keys, { progressCallback, untrusted, @@ -2559,606 +2391,504 @@ imported: keys.length }; } - - deleteKeysFromBackup(roomId, sessionId, version) { + async deleteKeysFromBackup(roomId, sessionId, version) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - const path = this.makeKeyBackupPath(roomId, sessionId, version); - return this.http.authedRequest(undefined, _httpApi.Method.Delete, path.path, path.queryData, undefined, { - prefix: _httpApi.PREFIX_UNSTABLE + await this.http.authedRequest(_httpApi.Method.Delete, path.path, path.queryData, undefined, { + prefix: _httpApi.ClientPrefix.V3 }); } + /** * Share shared-history decryption keys with the given users. * - * @param {string} roomId the room for which keys should be shared. - * @param {array} userIds a list of users to share with. The keys will be sent to + * @param roomId - the room for which keys should be shared. + * @param userIds - a list of users to share with. The keys will be sent to * all of the user's current devices. */ - - async sendSharedHistoryKeys(roomId, userIds) { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } - const roomEncryption = this.roomList.getRoomEncryption(roomId); - if (!roomEncryption) { // unknown room, or unencrypted room _logger.logger.error("Unknown room. Not sharing decryption keys"); - return; } - const deviceInfos = await this.crypto.downloadKeys(userIds); - const devicesByUser = {}; - - for (const [userId, devices] of Object.entries(deviceInfos)) { - devicesByUser[userId] = Object.values(devices); - } // XXX: Private member access - + const devicesByUser = new Map(); + for (const [userId, devices] of deviceInfos) { + devicesByUser.set(userId, Array.from(devices.values())); + } + // XXX: Private member access const alg = this.crypto.getRoomDecryptor(roomId, roomEncryption.algorithm); - if (alg.sendSharedHistoryInboundSessions) { await alg.sendSharedHistoryInboundSessions(devicesByUser); } else { _logger.logger.warn("Algorithm does not support sharing previous keys", roomEncryption.algorithm); } } + /** * Get the config for the media repository. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves with an object containing the config. + * @returns Promise which resolves with an object containing the config. */ - - - getMediaConfig(callback) { - return this.http.authedRequest(callback, _httpApi.Method.Get, "/config", undefined, undefined, { - prefix: _httpApi.PREFIX_MEDIA_R0 + getMediaConfig() { + return this.http.authedRequest(_httpApi.Method.Get, "/config", undefined, undefined, { + prefix: _httpApi.MediaPrefix.R0 }); } + /** * Get the room for the given room ID. * This function will return a valid room for any room for which a Room event * has been emitted. Note in particular that other events, eg. RoomState.members * will be emitted for a room before this function will return the given room. - * @param {string} roomId The room ID - * @return {Room|null} The Room or null if it doesn't exist or there is no data store. + * @param roomId - The room ID + * @returns The Room or null if it doesn't exist or there is no data store. */ - - getRoom(roomId) { + if (!roomId) { + return null; + } return this.store.getRoom(roomId); } + /** * Retrieve all known rooms. - * @return {Room[]} A list of rooms, or an empty list if there is no data store. + * @returns A list of rooms, or an empty list if there is no data store. */ - - getRooms() { return this.store.getRooms(); } + /** * Retrieve all rooms that should be displayed to the user * This is essentially getRooms() with some rooms filtered out, eg. old versions * of rooms that have been replaced or (in future) other rooms that have been * marked at the protocol level as not to be displayed to the user. - * @return {Room[]} A list of rooms, or an empty list if there is no data store. + * + * @param msc3946ProcessDynamicPredecessor - if true, look for an + * m.room.predecessor state event and + * use it if found (MSC3946). + * @returns A list of rooms, or an empty list if there is no data store. */ - - - getVisibleRooms() { + getVisibleRooms(msc3946ProcessDynamicPredecessor = false) { const allRooms = this.store.getRooms(); const replacedRooms = new Set(); - for (const r of allRooms) { - const createEvent = r.currentState.getStateEvents(_event2.EventType.RoomCreate, ''); // invites are included in this list and we don't know their create events yet - - if (createEvent) { - const predecessor = createEvent.getContent()['predecessor']; - - if (predecessor && predecessor['room_id']) { - replacedRooms.add(predecessor['room_id']); - } + const predecessor = r.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId; + if (predecessor) { + replacedRooms.add(predecessor); } } - return allRooms.filter(r => { - const tombstone = r.currentState.getStateEvents(_event2.EventType.RoomTombstone, ''); - + const tombstone = r.currentState.getStateEvents(_event2.EventType.RoomTombstone, ""); if (tombstone && replacedRooms.has(r.roomId)) { return false; } - return true; }); } + /** * Retrieve a user. - * @param {string} userId The user ID to retrieve. - * @return {?User} A user or null if there is no data store or the user does + * @param userId - The user ID to retrieve. + * @returns A user or null if there is no data store or the user does * not exist. */ - - getUser(userId) { return this.store.getUser(userId); } + /** * Retrieve all known users. - * @return {User[]} A list of users, or an empty list if there is no data store. + * @returns A list of users, or an empty list if there is no data store. */ - - getUsers() { return this.store.getUsers(); } + /** * Set account data event for the current user. * It will retry the request up to 5 times. - * @param {string} eventType The event type - * @param {Object} content the contents object for the event - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: an empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param eventType - The event type + * @param content - the contents object for the event + * @returns Promise which resolves: an empty object + * @returns Rejects: with an error response. */ - - - setAccountData(eventType, content, callback) { + setAccountData(eventType, content) { const path = utils.encodeUri("/user/$userId/account_data/$type", { $userId: this.credentials.userId, $type: eventType }); - const promise = (0, _httpApi.retryNetworkOperation)(5, () => { - return this.http.authedRequest(undefined, _httpApi.Method.Put, path, undefined, content); + return (0, _httpApi.retryNetworkOperation)(5, () => { + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, content); }); - - if (callback) { - promise.then(result => callback(null, result), callback); - } - - return promise; } + /** * Get account data event of given type for the current user. - * @param {string} eventType The event type - * @return {?object} The contents of the given account data event + * @param eventType - The event type + * @returns The contents of the given account data event */ - - getAccountData(eventType) { return this.store.getAccountData(eventType); } + /** * Get account data event of given type for the current user. This variant * gets account data directly from the homeserver if the local store is not * ready, which can be useful very early in startup before the initial sync. - * @param {string} eventType The event type - * @return {Promise} Resolves: The contents of the given account - * data event. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param eventType - The event type + * @returns Promise which resolves: The contents of the given account data event. + * @returns Rejects: with an error response. */ - - async getAccountDataFromServer(eventType) { if (this.isInitialSyncComplete()) { const event = this.store.getAccountData(eventType); - if (!event) { return null; - } // The network version below returns just the content, so this branch + } + // The network version below returns just the content, so this branch // does the same to match. - - return event.getContent(); } - const path = utils.encodeUri("/user/$userId/account_data/$type", { $userId: this.credentials.userId, $type: eventType }); - try { - return await this.http.authedRequest(undefined, _httpApi.Method.Get, path); + return await this.http.authedRequest(_httpApi.Method.Get, path); } catch (e) { - if (e.data?.errcode === 'M_NOT_FOUND') { + if (e.data?.errcode === "M_NOT_FOUND") { return null; } - throw e; } } + async deleteAccountData(eventType) { + const msc3391DeleteAccountDataServerSupport = this.canSupport.get(_feature.Feature.AccountDataDeletion); + // if deletion is not supported overwrite with empty content + if (msc3391DeleteAccountDataServerSupport === _feature.ServerSupport.Unsupported) { + await this.setAccountData(eventType, {}); + return; + } + const path = utils.encodeUri("/user/$userId/account_data/$type", { + $userId: this.getSafeUserId(), + $type: eventType + }); + const options = msc3391DeleteAccountDataServerSupport === _feature.ServerSupport.Unstable ? { + prefix: "/_matrix/client/unstable/org.matrix.msc3391" + } : undefined; + return await this.http.authedRequest(_httpApi.Method.Delete, path, undefined, undefined, options); + } + /** * Gets the users that are ignored by this client - * @returns {string[]} The array of users that are ignored (empty if none) + * @returns The array of users that are ignored (empty if none) */ - - getIgnoredUsers() { const event = this.getAccountData("m.ignored_user_list"); if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return []; return Object.keys(event.getContent()["ignored_users"]); } + /** * Sets the users that the current user should ignore. - * @param {string[]} userIds the user IDs to ignore - * @param {module:client.callback} [callback] Optional. - * @return {Promise} Resolves: an empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param userIds - the user IDs to ignore + * @returns Promise which resolves: an empty object + * @returns Rejects: with an error response. */ - - - setIgnoredUsers(userIds, callback) { + setIgnoredUsers(userIds) { const content = { ignored_users: {} }; userIds.forEach(u => { content.ignored_users[u] = {}; }); - return this.setAccountData("m.ignored_user_list", content, callback); + return this.setAccountData("m.ignored_user_list", content); } + /** * Gets whether or not a specific user is being ignored by this client. - * @param {string} userId the user ID to check - * @returns {boolean} true if the user is ignored, false otherwise + * @param userId - the user ID to check + * @returns true if the user is ignored, false otherwise */ - - isUserIgnored(userId) { return this.getIgnoredUsers().includes(userId); } + /** * Join a room. If you have already joined the room, this will no-op. - * @param {string} roomIdOrAlias The room ID or room alias to join. - * @param {Object} opts Options when joining the room. - * @param {boolean} opts.syncRoom True to do a room initial sync on the resulting - * room. If false, the returned Room object will have no current state. - * Default: true. - * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, the signing URL is passed in this parameter. - * @param {string[]} opts.viaServers The server names to try and join through in addition to those that are automatically chosen. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Room object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param roomIdOrAlias - The room ID or room alias to join. + * @param opts - Options when joining the room. + * @returns Promise which resolves: Room object. + * @returns Rejects: with an error response. */ - - - async joinRoom(roomIdOrAlias, opts, callback) { - // to help people when upgrading.. - if (utils.isFunction(opts)) { - throw new Error("Expected 'opts' object, got function."); - } - - opts = opts || {}; - + async joinRoom(roomIdOrAlias, opts = {}) { if (opts.syncRoom === undefined) { opts.syncRoom = true; } - const room = this.getRoom(roomIdOrAlias); - - if (room && room.hasMembershipState(this.credentials.userId, "join")) { + if (room?.hasMembershipState(this.credentials.userId, "join")) { return Promise.resolve(room); } - let signPromise = Promise.resolve(); - if (opts.inviteSignUrl) { - signPromise = this.http.requestOtherUrl(undefined, _httpApi.Method.Post, opts.inviteSignUrl, { - mxid: this.credentials.userId - }); + const url = new URL(opts.inviteSignUrl); + url.searchParams.set("mxid", this.credentials.userId); + signPromise = this.http.requestOtherUrl(_httpApi.Method.Post, url); } - const queryString = {}; - if (opts.viaServers) { queryString["server_name"] = opts.viaServers; } - - const reqOpts = { - qsStringifyOptions: { - arrayFormat: 'repeat' - } - }; - try { const data = {}; const signedInviteObj = await signPromise; - if (signedInviteObj) { data.third_party_signed = signedInviteObj; } - const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias }); - const res = await this.http.authedRequest(undefined, _httpApi.Method.Post, path, queryString, data, reqOpts); - const roomId = res['room_id']; - const syncApi = new _sync.SyncApi(this, this.clientOpts); + const res = await this.http.authedRequest(_httpApi.Method.Post, path, queryString, data); + const roomId = res.room_id; + const syncApi = new _sync.SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); const room = syncApi.createRoom(roomId); - - if (opts.syncRoom) {// v2 will do this for us + if (opts.syncRoom) { + // v2 will do this for us // return syncApi.syncRoom(room); } - - callback?.(null, room); return room; } catch (e) { - callback?.(e); throw e; // rethrow for reject } } + /** * Resend an event. Will also retry any to-device messages waiting to be sent. - * @param {MatrixEvent} event The event to resend. - * @param {Room} room Optional. The room the event is in. Will update the + * @param event - The event to resend. + * @param room - Optional. The room the event is in. Will update the * timeline entry if provided. - * @return {Promise} Resolves: to an ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an ISendEventResponse object + * @returns Rejects: with an error response. */ - - resendEvent(event, room) { // also kick the to-device queue to retry this.toDeviceMessageQueue.sendQueue(); this.updatePendingEventStatus(room, event, _event.EventStatus.SENDING); return this.encryptAndSendEvent(room, event); } + /** * Cancel a queued or unsent event. * - * @param {MatrixEvent} event Event to cancel + * @param event - Event to cancel * @throws Error if the event is not in QUEUED, NOT_SENT or ENCRYPTING state */ - - cancelPendingEvent(event) { if (![_event.EventStatus.QUEUED, _event.EventStatus.NOT_SENT, _event.EventStatus.ENCRYPTING].includes(event.status)) { throw new Error("cannot cancel an event with status " + event.status); - } // if the event is currently being encrypted then - + } + // if the event is currently being encrypted then if (event.status === _event.EventStatus.ENCRYPTING) { this.pendingEventEncryption.delete(event.getId()); } else if (this.scheduler && event.status === _event.EventStatus.QUEUED) { // tell the scheduler to forget about it, if it's queued this.scheduler.removeEventFromQueue(event); - } // then tell the room about the change of state, which will remove it - // from the room's list of pending events. - + } + // then tell the room about the change of state, which will remove it + // from the room's list of pending events. const room = this.getRoom(event.getRoomId()); this.updatePendingEventStatus(room, event, _event.EventStatus.CANCELLED); } + /** - * @param {string} roomId - * @param {string} name - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - setRoomName(roomId, name, callback) { + setRoomName(roomId, name) { return this.sendStateEvent(roomId, _event2.EventType.RoomName, { name: name - }, undefined, callback); + }); } + /** - * @param {string} roomId - * @param {string} topic - * @param {string} htmlTopic Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param htmlTopic - Optional. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - setRoomTopic(roomId, topic, htmlTopicOrCallback) { - const isCallback = typeof htmlTopicOrCallback === 'function'; - const htmlTopic = isCallback ? undefined : htmlTopicOrCallback; - const callback = isCallback ? htmlTopicOrCallback : undefined; + setRoomTopic(roomId, topic, htmlTopic) { const content = ContentHelpers.makeTopicContent(topic, htmlTopic); - return this.sendStateEvent(roomId, _event2.EventType.RoomTopic, content, undefined, callback); + return this.sendStateEvent(roomId, _event2.EventType.RoomTopic, content); } + /** - * @param {string} roomId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an object keyed by tagId with objects containing a numeric order field. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an object keyed by tagId with objects containing a numeric order field. + * @returns Rejects: with an error response. */ - - - getRoomTags(roomId, callback) { + getRoomTags(roomId) { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", { $userId: this.credentials.userId, $roomId: roomId }); - return this.http.authedRequest(callback, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** - * @param {string} roomId - * @param {string} tagName name of room tag to be set - * @param {object} metadata associated with that tag to be stored - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param tagName - name of room tag to be set + * @param metadata - associated with that tag to be stored + * @returns Promise which resolves: to an empty object + * @returns Rejects: with an error response. */ - - - setRoomTag(roomId, tagName, metadata, callback) { + setRoomTag(roomId, tagName, metadata) { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { $userId: this.credentials.userId, $roomId: roomId, $tag: tagName }); - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, metadata); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, metadata); } + /** - * @param {string} roomId - * @param {string} tagName name of room tag to be removed - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: void - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param tagName - name of room tag to be removed + * @returns Promise which resolves: to an empty object + * @returns Rejects: with an error response. */ - - - deleteRoomTag(roomId, tagName, callback) { + deleteRoomTag(roomId, tagName) { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { $userId: this.credentials.userId, $roomId: roomId, $tag: tagName }); - return this.http.authedRequest(callback, _httpApi.Method.Delete, path); + return this.http.authedRequest(_httpApi.Method.Delete, path); } + /** - * @param {string} roomId - * @param {string} eventType event type to be set - * @param {object} content event content - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param eventType - event type to be set + * @param content - event content + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - - setRoomAccountData(roomId, eventType, content, callback) { + setRoomAccountData(roomId, eventType, content) { const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", { $userId: this.credentials.userId, $roomId: roomId, $type: eventType }); - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, content); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, content); } + /** - * Set a user's power level. - * @param {string} roomId - * @param {string} userId - * @param {Number} powerLevel - * @param {MatrixEvent} event - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * Set a power level to one or multiple users. + * @returns Promise which resolves: to an ISendEventResponse object + * @returns Rejects: with an error response. */ - - - setPowerLevel(roomId, userId, powerLevel, event, callback) { + setPowerLevel(roomId, userId, powerLevel, event) { let content = { users: {} }; - if (event?.getType() === _event2.EventType.RoomPowerLevels) { // take a copy of the content to ensure we don't corrupt // existing client state with a failed power level change content = utils.deepCopy(event.getContent()); } - - content.users[userId] = powerLevel; + const users = Array.isArray(userId) ? userId : [userId]; + for (const user of users) { + if (powerLevel == null) { + delete content.users[user]; + } else { + content.users[user] = powerLevel; + } + } const path = utils.encodeUri("/rooms/$roomId/state/m.room.power_levels", { $roomId: roomId }); - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, content); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, content); } + /** * Create an m.beacon_info event - * @param {string} roomId - * @param {MBeaconInfoEventContent} beaconInfoContent - * @returns {ISendEventResponse} + * @returns */ // eslint-disable-next-line @typescript-eslint/naming-convention - - async unstable_createLiveBeacon(roomId, beaconInfoContent) { return this.unstable_setLiveBeacon(roomId, beaconInfoContent); } + /** * Upsert a live beacon event * using a specific m.beacon_info.* event variable type - * @param {string} roomId string - * @param {MBeaconInfoEventContent} beaconInfoContent - * @returns {ISendEventResponse} + * @param roomId - string + * @returns */ // eslint-disable-next-line @typescript-eslint/naming-convention - - async unstable_setLiveBeacon(roomId, beaconInfoContent) { - const userId = this.getUserId(); - return this.sendStateEvent(roomId, _beacon.M_BEACON_INFO.name, beaconInfoContent, userId); + return this.sendStateEvent(roomId, _beacon.M_BEACON_INFO.name, beaconInfoContent, this.getUserId()); } - /** - * @param {string} roomId - * @param {string} threadId - * @param {string} eventType - * @param {Object} content - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ - - - sendEvent(roomId, threadId, eventType, content, txnId, callback) { - if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = txnId; - txnId = content; - content = eventType; - eventType = threadId; + sendEvent(roomId, threadIdOrEventType, eventTypeOrContent, contentOrTxnId, txnIdOrVoid) { + let threadId; + let eventType; + let content; + let txnId; + if (!threadIdOrEventType?.startsWith(EVENT_ID_PREFIX) && threadIdOrEventType !== null) { + txnId = contentOrTxnId; + content = eventTypeOrContent; + eventType = threadIdOrEventType; threadId = null; - } // If we expect that an event is part of a thread but is missing the relation - // we need to add it manually, as well as the reply fallback - + } else { + txnId = txnIdOrVoid; + content = contentOrTxnId; + eventType = eventTypeOrContent; + threadId = threadIdOrEventType; + } + // If we expect that an event is part of a thread but is missing the relation + // we need to add it manually, as well as the reply fallback if (threadId && !content["m.relates_to"]?.rel_type) { const isReply = !!content["m.relates_to"]?.["m.in_reply_to"]; content["m.relates_to"] = _objectSpread(_objectSpread({}, content["m.relates_to"]), {}, { - "rel_type": _thread.THREAD_RELATION_TYPE.name, - "event_id": threadId, + rel_type: _thread.THREAD_RELATION_TYPE.name, + event_id: threadId, // Set is_falling_back to true unless this is actually intended to be a reply - "is_falling_back": !isReply + is_falling_back: !isReply }); const thread = this.getRoom(roomId)?.getThread(threadId); - if (thread && !isReply) { content["m.relates_to"]["m.in_reply_to"] = { - "event_id": thread.lastReply(ev => { + event_id: thread.lastReply(ev => { return ev.isRelation(_thread.THREAD_RELATION_TYPE.name) && !ev.status; })?.getId() ?? threadId }; } } - return this.sendCompleteEvent(roomId, threadId, { type: eventType, content - }, txnId, callback); + }, txnId); } + /** - * @param {string} roomId - * @param {string} threadId - * @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added. - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param eventObject - An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added. + * @param txnId - Optional. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - - sendCompleteEvent(roomId, threadId, eventObject, txnId, callback) { - if (utils.isFunction(txnId)) { - callback = txnId; // convert for legacy - - txnId = undefined; - } - + sendCompleteEvent(roomId, threadId, eventObject, txnId) { if (!txnId) { txnId = this.makeTxnId(); - } // We always construct a MatrixEvent when sending because the store and scheduler use them. - // We'll extract the params back out if it turns out the client has no scheduler or store. - + } + // We always construct a MatrixEvent when sending because the store and scheduler use them. + // We'll extract the params back out if it turns out the client has no scheduler or store. const localEvent = new _event.MatrixEvent(Object.assign(eventObject, { event_id: "~" + roomId + ":" + txnId, user_id: this.credentials.userId, @@ -3167,62 +2897,54 @@ origin_server_ts: new Date().getTime() })); const room = this.getRoom(roomId); - const thread = room?.getThread(threadId); - + const thread = threadId ? room?.getThread(threadId) : undefined; if (thread) { localEvent.setThread(thread); - } // set up re-emitter for this new event - this is normally the job of EventMapper but we don't use it here - + } + // set up re-emitter for this new event - this is normally the job of EventMapper but we don't use it here this.reEmitter.reEmit(localEvent, [_event.MatrixEventEvent.Replaced, _event.MatrixEventEvent.VisibilityChange]); - room?.reEmitter.reEmit(localEvent, [_event.MatrixEventEvent.BeforeRedaction]); // if this is a relation or redaction of an event + room?.reEmitter.reEmit(localEvent, [_event.MatrixEventEvent.BeforeRedaction]); + + // if this is a relation or redaction of an event // that hasn't been sent yet (e.g. with a local id starting with a ~) // then listen for the remote echo of that event so that by the time // this event does get sent, we have the correct event_id - const targetId = localEvent.getAssociatedId(); - if (targetId?.startsWith("~")) { - const target = room.getPendingEvents().find(e => e.getId() === targetId); - target.once(_event.MatrixEventEvent.LocalEventIdReplaced, () => { + const target = room?.getPendingEvents().find(e => e.getId() === targetId); + target?.once(_event.MatrixEventEvent.LocalEventIdReplaced, () => { localEvent.updateAssociatedId(target.getId()); }); } - const type = localEvent.getType(); - _logger.logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`); - localEvent.setTxnId(txnId); - localEvent.setStatus(_event.EventStatus.SENDING); // add this event immediately to the local store as 'sending'. + localEvent.setStatus(_event.EventStatus.SENDING); - room?.addPendingEvent(localEvent, txnId); // addPendingEvent can change the state to NOT_SENT if it believes + // add this event immediately to the local store as 'sending'. + room?.addPendingEvent(localEvent, txnId); + + // addPendingEvent can change the state to NOT_SENT if it believes // that there's other events that have failed. We won't bother to // try sending the event if the state has changed as such. - if (localEvent.status === _event.EventStatus.NOT_SENT) { return Promise.reject(new Error("Event blocked by other events not yet sent")); } - - return this.encryptAndSendEvent(room, localEvent, callback); + return this.encryptAndSendEvent(room, localEvent); } + /** * encrypts the event if necessary; adds the event to the queue, or sends it; marks the event as sent/unsent - * @param room - * @param event - * @param callback - * @returns {Promise} returns a promise which resolves with the result of the send request - * @private + * @returns returns a promise which resolves with the result of the send request */ - - - encryptAndSendEvent(room, event, callback) { - let cancelled = false; // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, + encryptAndSendEvent(room, event) { + let cancelled = false; + // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, // so that we can handle synchronous and asynchronous exceptions with the // same code path. - return Promise.resolve().then(() => { - const encryptionPromise = this.encryptEventIfNeeded(event, room); + const encryptionPromise = this.encryptEventIfNeeded(event, room ?? undefined); if (!encryptionPromise) return null; // doesn't need encryption this.pendingEventEncryption.set(event.getId(), encryptionPromise); @@ -3233,63 +2955,50 @@ cancelled = true; return; } - this.updatePendingEventStatus(room, event, _event.EventStatus.SENDING); }); }).then(() => { if (cancelled) return {}; - let promise; - + let promise = null; if (this.scheduler) { // if this returns a promise then the scheduler has control now and will // resolve/reject when it is done. Internally, the scheduler will invoke // processFn which is set to this._sendEventHttpRequest so the same code // path is executed regardless. promise = this.scheduler.queueEvent(event); - if (promise && this.scheduler.getQueueForEvent(event).length > 1) { // event is processed FIFO so if the length is 2 or more we know // this event is stuck behind an earlier event. this.updatePendingEventStatus(room, event, _event.EventStatus.QUEUED); } } - if (!promise) { promise = this.sendEventHttpRequest(event); - if (room) { promise = promise.then(res => { - room.updatePendingEvent(event, _event.EventStatus.SENT, res['event_id']); + room.updatePendingEvent(event, _event.EventStatus.SENT, res["event_id"]); return res; }); } } - return promise; - }).then(res => { - callback?.(null, res); - return res; }).catch(err => { _logger.logger.error("Error sending event", err.stack || err); - try { // set the error on the event before we update the status: // updating the status emits the event, so the state should be // consistent at that point. event.error = err; - this.updatePendingEventStatus(room, event, _event.EventStatus.NOT_SENT); // also put the event object on the error: the caller will need this - // to resend or cancel the event - - err.event = event; - callback?.(err); + this.updatePendingEventStatus(room, event, _event.EventStatus.NOT_SENT); } catch (e) { _logger.logger.error("Exception in error handler!", e.stack || err); } - + if (err instanceof _httpApi.MatrixError) { + err.event = event; + } throw err; }); } - encryptEventIfNeeded(event, room) { if (event.isEncrypted()) { // this event has already been encrypted; this happens if the @@ -3297,24 +3006,20 @@ // attempt. return null; } - if (event.isRedaction()) { // Redactions do not support encryption in the spec at this time, // whilst it mostly worked in some clients, it wasn't compliant. return null; } - - if (!this.isRoomEncrypted(event.getRoomId())) { + if (!room || !this.isRoomEncrypted(event.getRoomId())) { return null; } - - if (!this.crypto && this.usingExternalCrypto) { + if (!this.cryptoBackend && this.usingExternalCrypto) { // The client has opted to allow sending messages to encrypted // rooms even if the room is encrypted, and we haven't setup // crypto. This is useful for users of matrix-org/pantalaimon return null; } - if (event.getType() === _event2.EventType.Reaction) { // For reactions, there is a very little gained by encrypting the entire // event, as relation data is already kept in the clear. Event @@ -3328,27 +3033,23 @@ // See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642 return null; } - - if (!this.crypto) { - throw new Error("This room is configured to use encryption, but your client does " + "not support encryption."); + if (!this.cryptoBackend) { + throw new Error("This room is configured to use encryption, but your client does not support encryption."); } - - return this.crypto.encryptEvent(event, room); + return this.cryptoBackend.encryptEvent(event, room); } + /** * Returns the eventType that should be used taking encryption into account * for a given eventType. - * @param {string} roomId the room for the events `eventType` relates to - * @param {string} eventType the event type - * @return {string} the event type taking encryption into account + * @param roomId - the room for the events `eventType` relates to + * @param eventType - the event type + * @returns the event type taking encryption into account */ - - getEncryptedIfNeededEventType(roomId, eventType) { if (eventType === _event2.EventType.Reaction) return eventType; return this.isRoomEncrypted(roomId) ? _event2.EventType.RoomMessageEncrypted : eventType; } - updatePendingEventStatus(room, event, newStatus) { if (room) { room.updatePendingEvent(event, newStatus); @@ -3356,15 +3057,12 @@ event.setStatus(newStatus); } } - sendEventHttpRequest(event) { let txnId = event.getTxnId(); - if (!txnId) { txnId = this.makeTxnId(); event.setTxnId(txnId); } - const pathParams = { $roomId: event.getRoomId(), $eventType: event.getWireType(), @@ -3372,351 +3070,220 @@ $txnId: txnId }; let path; - if (event.isState()) { let pathTemplate = "/rooms/$roomId/state/$eventType"; - if (event.getStateKey() && event.getStateKey().length > 0) { pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey"; } - path = utils.encodeUri(pathTemplate, pathParams); } else if (event.isRedaction()) { const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`; - path = utils.encodeUri(pathTemplate, Object.assign({ + path = utils.encodeUri(pathTemplate, _objectSpread({ $redactsEventId: event.event.redacts }, pathParams)); } else { path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams); } - - return this.http.authedRequest(undefined, _httpApi.Method.Put, path, undefined, event.getWireContent()).then(res => { + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, event.getWireContent()).then(res => { _logger.logger.log(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`); - return res; }); } + /** - * @param {string} roomId - * @param {string} eventId - * @param {string} [txnId] transaction id. One will be made up if not - * supplied. - * @param {object|module:client.callback} cbOrOpts - * Options to pass on, may contain `reason`. - * Can be callback for backwards compatibility. Deprecated - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param txnId - transaction id. One will be made up if not supplied. + * @param opts - Options to pass on, may contain `reason` and `with_relations` (MSC3912) + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. + * @throws Error if called with `with_relations` (MSC3912) but the server does not support it. + * Callers should check whether the server supports MSC3912 via `MatrixClient.canSupport`. */ - - redactEvent(roomId, threadId, eventId, txnId, cbOrOpts) { + redactEvent(roomId, threadId, eventId, txnId, opts) { if (!eventId?.startsWith(EVENT_ID_PREFIX)) { - cbOrOpts = txnId; + opts = txnId; txnId = eventId; eventId = threadId; threadId = null; } - - const opts = typeof cbOrOpts === 'object' ? cbOrOpts : {}; - const reason = opts.reason; - const callback = typeof cbOrOpts === 'function' ? cbOrOpts : undefined; + const reason = opts?.reason; + if (opts?.with_relations && this.canSupport.get(_feature.Feature.RelationBasedRedactions) === _feature.ServerSupport.Unsupported) { + throw new Error("Server does not support relation based redactions " + `roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId ${threadId}`); + } + const withRelations = opts?.with_relations ? { + [this.canSupport.get(_feature.Feature.RelationBasedRedactions) === _feature.ServerSupport.Stable ? _event2.MSC3912_RELATION_BASED_REDACTIONS_PROP.stable : _event2.MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable]: opts?.with_relations + } : {}; return this.sendCompleteEvent(roomId, threadId, { type: _event2.EventType.RoomRedaction, - content: { + content: _objectSpread(_objectSpread({}, withRelations), {}, { reason - }, + }), redacts: eventId - }, txnId, callback); + }, txnId); } + /** - * @param {string} roomId - * @param {string} threadId - * @param {Object} content - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to an ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param txnId - Optional. + * @returns Promise which resolves: to an ISendEventResponse object + * @returns Rejects: with an error response. */ - - sendMessage(roomId, threadId, content, txnId, callback) { + sendMessage(roomId, threadId, content, txnId) { if (typeof threadId !== "string" && threadId !== null) { - callback = txnId; txnId = content; content = threadId; threadId = null; } + const eventType = _event2.EventType.RoomMessage; + const sendContent = content; + return this.sendEvent(roomId, threadId, eventType, sendContent, txnId); + } - if (utils.isFunction(txnId)) { - callback = txnId; // for legacy - - txnId = undefined; - } // Populate all outbound events with Extensible Events metadata to ensure there's a - // reasonably large pool of messages to parse. - - - let eventType = _event2.EventType.RoomMessage; - let sendContent = content; - - const makeContentExtensible = (content = {}, recurse = true) => { - let newEvent = null; + /** + * @param txnId - Optional. + * @returns + * @returns Rejects: with an error response. + */ - if (content['msgtype'] === _event2.MsgType.Text) { - newEvent = _matrixEventsSdk.MessageEvent.from(content['body'], content['formatted_body']).serialize(); - } else if (content['msgtype'] === _event2.MsgType.Emote) { - newEvent = _matrixEventsSdk.EmoteEvent.from(content['body'], content['formatted_body']).serialize(); - } else if (content['msgtype'] === _event2.MsgType.Notice) { - newEvent = _matrixEventsSdk.NoticeEvent.from(content['body'], content['formatted_body']).serialize(); - } - - if (newEvent && content['m.new_content'] && recurse) { - const newContent = makeContentExtensible(content['m.new_content'], false); - - if (newContent) { - newEvent.content['m.new_content'] = newContent.content; - } - } - - if (newEvent) { - // copy over all other fields we don't know about - for (const [k, v] of Object.entries(content)) { - if (!newEvent.content.hasOwnProperty(k)) { - newEvent.content[k] = v; - } - } - } - - return newEvent; - }; - - const result = makeContentExtensible(sendContent); - - if (result) { - eventType = result.type; - sendContent = result.content; - } - - return this.sendEvent(roomId, threadId, eventType, sendContent, txnId, callback); - } - /** - * @param {string} roomId - * @param {string} threadId - * @param {string} body - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ - - - sendTextMessage(roomId, threadId, body, txnId, callback) { + sendTextMessage(roomId, threadId, body, txnId) { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = txnId; txnId = body; body = threadId; threadId = null; } - const content = ContentHelpers.makeTextMessage(body); - return this.sendMessage(roomId, threadId, content, txnId, callback); + return this.sendMessage(roomId, threadId, content, txnId); } + /** - * @param {string} roomId - * @param {string} threadId - * @param {string} body - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to a ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param txnId - Optional. + * @returns Promise which resolves: to a ISendEventResponse object + * @returns Rejects: with an error response. */ - - sendNotice(roomId, threadId, body, txnId, callback) { + sendNotice(roomId, threadId, body, txnId) { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = txnId; txnId = body; body = threadId; threadId = null; } - const content = ContentHelpers.makeNotice(body); - return this.sendMessage(roomId, threadId, content, txnId, callback); + return this.sendMessage(roomId, threadId, content, txnId); } + /** - * @param {string} roomId - * @param {string} threadId - * @param {string} body - * @param {string} txnId Optional. - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to a ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param txnId - Optional. + * @returns Promise which resolves: to a ISendEventResponse object + * @returns Rejects: with an error response. */ - - sendEmoteMessage(roomId, threadId, body, txnId, callback) { + sendEmoteMessage(roomId, threadId, body, txnId) { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = txnId; txnId = body; body = threadId; threadId = null; } - const content = ContentHelpers.makeEmoteMessage(body); - return this.sendMessage(roomId, threadId, content, txnId, callback); + return this.sendMessage(roomId, threadId, content, txnId); } + /** - * @param {string} roomId - * @param {string} threadId - * @param {string} url - * @param {Object} info - * @param {string} text - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to a ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to a ISendEventResponse object + * @returns Rejects: with an error response. */ - - sendImageMessage(roomId, threadId, url, info, text = "Image", callback) { + sendImageMessage(roomId, threadId, url, info, text = "Image") { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = text; text = info || "Image"; info = url; url = threadId; threadId = null; } - - if (utils.isFunction(text)) { - callback = text; // legacy - - text = undefined; - } - const content = { msgtype: _event2.MsgType.Image, url: url, info: info, body: text }; - return this.sendMessage(roomId, threadId, content, undefined, callback); + return this.sendMessage(roomId, threadId, content); } + /** - * @param {string} roomId - * @param {string} threadId - * @param {string} url - * @param {Object} info - * @param {string} text - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to a ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to a ISendEventResponse object + * @returns Rejects: with an error response. */ - - sendStickerMessage(roomId, threadId, url, info, text = "Sticker", callback) { + sendStickerMessage(roomId, threadId, url, info, text = "Sticker") { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = text; text = info || "Sticker"; info = url; url = threadId; threadId = null; } - - if (utils.isFunction(text)) { - callback = text; // legacy - - text = undefined; - } - const content = { url: url, info: info, body: text }; - return this.sendEvent(roomId, threadId, _event2.EventType.Sticker, content, undefined, callback); + return this.sendEvent(roomId, threadId, _event2.EventType.Sticker, content); } + /** - * @param {string} roomId - * @param {string} threadId - * @param {string} body - * @param {string} htmlBody - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to a ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to a ISendEventResponse object + * @returns Rejects: with an error response. */ - - sendHtmlMessage(roomId, threadId, body, htmlBody, callback) { + sendHtmlMessage(roomId, threadId, body, htmlBody) { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = htmlBody; htmlBody = body; body = threadId; threadId = null; } - const content = ContentHelpers.makeHtmlMessage(body, htmlBody); - return this.sendMessage(roomId, threadId, content, undefined, callback); + return this.sendMessage(roomId, threadId, content); } + /** - * @param {string} roomId - * @param {string} body - * @param {string} htmlBody - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to a ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to a ISendEventResponse object + * @returns Rejects: with an error response. */ - - sendHtmlNotice(roomId, threadId, body, htmlBody, callback) { + sendHtmlNotice(roomId, threadId, body, htmlBody) { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = htmlBody; htmlBody = body; body = threadId; threadId = null; } - const content = ContentHelpers.makeHtmlNotice(body, htmlBody); - return this.sendMessage(roomId, threadId, content, undefined, callback); + return this.sendMessage(roomId, threadId, content); } + /** - * @param {string} roomId - * @param {string} threadId - * @param {string} body - * @param {string} htmlBody - * @param {module:client.callback} callback Optional. Deprecated - * @return {Promise} Resolves: to a ISendEventResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to a ISendEventResponse object + * @returns Rejects: with an error response. */ - - sendHtmlEmote(roomId, threadId, body, htmlBody, callback) { + sendHtmlEmote(roomId, threadId, body, htmlBody) { if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - callback = htmlBody; htmlBody = body; body = threadId; threadId = null; } - const content = ContentHelpers.makeHtmlEmote(body, htmlBody); - return this.sendMessage(roomId, threadId, content, undefined, callback); + return this.sendMessage(roomId, threadId, content); } + /** * Send a receipt. - * @param {Event} event The event being acknowledged - * @param {ReceiptType} receiptType The kind of receipt e.g. "m.read". Other than + * @param event - The event being acknowledged + * @param receiptType - The kind of receipt e.g. "m.read". Other than * ReceiptType.Read are experimental! - * @param {object} body Additional content to send alongside the receipt. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param body - Additional content to send alongside the receipt. + * @param unthreaded - An unthreaded receipt will clear room+thread notifications + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - - sendReceipt(event, receiptType, body, callback) { - if (typeof body === 'function') { - callback = body; // legacy - - body = {}; - } - + async sendReceipt(event, receiptType, body, unthreaded = false) { if (this.isGuest()) { return Promise.resolve({}); // guests cannot send receipts so don't bother. } @@ -3726,416 +3293,353 @@ $receiptType: receiptType, $eventId: event.getId() }); - const promise = this.http.authedRequest(callback, _httpApi.Method.Post, path, undefined, body || {}); + if (!unthreaded) { + const isThread = !!event.threadRootId; + body = _objectSpread(_objectSpread({}, body), {}, { + thread_id: isThread ? event.threadRootId : _read_receipts.MAIN_ROOM_TIMELINE + }); + } + const promise = this.http.authedRequest(_httpApi.Method.Post, path, undefined, body || {}); const room = this.getRoom(event.getRoomId()); - - if (room) { + if (room && this.credentials.userId) { room.addLocalEchoReceipt(this.credentials.userId, event, receiptType); } - return promise; } + /** * Send a read receipt. - * @param {Event} event The event that has been read. - * @param {ReceiptType} receiptType other than ReceiptType.Read are experimental! Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param event - The event that has been read. + * @param receiptType - other than ReceiptType.Read are experimental! Optional. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - - async sendReadReceipt(event, receiptType = _read_receipts.ReceiptType.Read, callback) { + async sendReadReceipt(event, receiptType = _read_receipts.ReceiptType.Read, unthreaded = false) { + if (!event) return; const eventId = event.getId(); const room = this.getRoom(event.getRoomId()); - - if (room && room.hasPendingEvent(eventId)) { + if (room?.hasPendingEvent(eventId)) { throw new Error(`Cannot set read receipt to a pending event (${eventId})`); } - - return this.sendReceipt(event, receiptType, {}, callback); + return this.sendReceipt(event, receiptType, {}, unthreaded); } + /** * Set a marker to indicate the point in a room before which the user has read every * event. This can be retrieved from room account data (the event type is `m.fully_read`) * and displayed as a horizontal line in the timeline that is visually distinct to the * position of the user's own read receipt. - * @param {string} roomId ID of the room that has been read - * @param {string} rmEventId ID of the event that has been read - * @param {MatrixEvent} rrEvent the event tracked by the read receipt. This is here for + * @param roomId - ID of the room that has been read + * @param rmEventId - ID of the event that has been read + * @param rrEvent - the event tracked by the read receipt. This is here for * convenience because the RR and the RM are commonly updated at the same time as each * other. The local echo of this receipt will be done if set. Optional. - * @param {MatrixEvent} rpEvent the m.read.private read receipt event for when we don't + * @param rpEvent - the m.read.private read receipt event for when we don't * want other users to see the read receipts. This is experimental. Optional. - * @return {Promise} Resolves: the empty object, {}. + * @returns Promise which resolves: the empty object, `{}`. */ - - async setRoomReadMarkers(roomId, rmEventId, rrEvent, rpEvent) { const room = this.getRoom(roomId); - if (room && room.hasPendingEvent(rmEventId)) { throw new Error(`Cannot set read marker to a pending event (${rmEventId})`); - } // Add the optional RR update, do local echo like `sendReceipt` - + } + // Add the optional RR update, do local echo like `sendReceipt` let rrEventId; - if (rrEvent) { rrEventId = rrEvent.getId(); - if (room?.hasPendingEvent(rrEventId)) { throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`); } - room?.addLocalEchoReceipt(this.credentials.userId, rrEvent, _read_receipts.ReceiptType.Read); - } // Add the optional private RR update, do local echo like `sendReceipt` - + } + // Add the optional private RR update, do local echo like `sendReceipt` let rpEventId; - if (rpEvent) { rpEventId = rpEvent.getId(); - if (room?.hasPendingEvent(rpEventId)) { throw new Error(`Cannot set read receipt to a pending event (${rpEventId})`); } - room?.addLocalEchoReceipt(this.credentials.userId, rpEvent, _read_receipts.ReceiptType.ReadPrivate); } - return await this.setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, rpEventId); } + /** * Get a preview of the given URL as of (roughly) the given point in time, * described as an object with OpenGraph keys and associated values. * Attributes may be synthesized where actual OG metadata is lacking. * Caches results to prevent hammering the server. - * @param {string} url The URL to get preview data for - * @param {Number} ts The preferred point in time that the preview should + * @param url - The URL to get preview data for + * @param ts - The preferred point in time that the preview should * describe (ms since epoch). The preview returned will either be the most * recent one preceding this timestamp if available, or failing that the next * most recent available preview. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Object of OG metadata. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Object of OG metadata. + * @returns Rejects: with an error response. * May return synthesized attributes if the URL lacked OG meta. */ - - - getUrlPreview(url, ts, callback) { + getUrlPreview(url, ts) { // bucket the timestamp to the nearest minute to prevent excessive spam to the server // Surely 60-second accuracy is enough for anyone. ts = Math.floor(ts / 60000) * 60000; const parsed = new URL(url); parsed.hash = ""; // strip the hash as it won't affect the preview - url = parsed.toString(); - const key = ts + "_" + url; // If there's already a request in flight (or we've handled it), return that instead. + const key = ts + "_" + url; + // If there's already a request in flight (or we've handled it), return that instead. const cachedPreview = this.urlPreviewCache[key]; - if (cachedPreview) { - if (callback) { - cachedPreview.then(callback).catch(callback); - } - return cachedPreview; } - - const resp = this.http.authedRequest(callback, _httpApi.Method.Get, "/preview_url", { + const resp = this.http.authedRequest(_httpApi.Method.Get, "/preview_url", { url, ts: ts.toString() }, undefined, { - prefix: _httpApi.PREFIX_MEDIA_R0 - }); // TODO: Expire the URL preview cache sometimes - + prefix: _httpApi.MediaPrefix.R0 + }); + // TODO: Expire the URL preview cache sometimes this.urlPreviewCache[key] = resp; return resp; } + /** - * @param {string} roomId - * @param {boolean} isTyping - * @param {Number} timeoutMs - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - - sendTyping(roomId, isTyping, timeoutMs, callback) { + sendTyping(roomId, isTyping, timeoutMs) { if (this.isGuest()) { return Promise.resolve({}); // guests cannot send typing notifications so don't bother. } const path = utils.encodeUri("/rooms/$roomId/typing/$userId", { $roomId: roomId, - $userId: this.credentials.userId + $userId: this.getUserId() }); const data = { typing: isTyping }; - if (isTyping) { data.timeout = timeoutMs ? timeoutMs : 20000; } - - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, data); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, data); } + /** * Determines the history of room upgrades for a given room, as far as the * client can see. Returns an array of Rooms where the first entry is the * oldest and the last entry is the newest (likely current) room. If the * provided room is not found, this returns an empty list. This works in * both directions, looking for older and newer rooms of the given room. - * @param {string} roomId The room ID to search from - * @param {boolean} verifyLinks If true, the function will only return rooms + * @param roomId - The room ID to search from + * @param verifyLinks - If true, the function will only return rooms * which can be proven to be linked. For example, rooms which have a create * event pointing to an old room which the client is not aware of or doesn't * have a matching tombstone would not be returned. - * @return {Room[]} An array of rooms representing the upgrade + * @param msc3946ProcessDynamicPredecessor - if true, look for + * m.room.predecessor state events as well as create events, and prefer + * predecessor events where they exist (MSC3946). + * @returns An array of rooms representing the upgrade * history. */ - - - getRoomUpgradeHistory(roomId, verifyLinks = false) { - let currentRoom = this.getRoom(roomId); + getRoomUpgradeHistory(roomId, verifyLinks = false, msc3946ProcessDynamicPredecessor = false) { + const currentRoom = this.getRoom(roomId); if (!currentRoom) return []; - const upgradeHistory = [currentRoom]; // Work backwards first, looking at create events. - - let createEvent = currentRoom.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); - - while (createEvent) { - const predecessor = createEvent.getContent()['predecessor']; - - if (predecessor && predecessor['room_id']) { - const refRoom = this.getRoom(predecessor['room_id']); - if (!refRoom) break; // end of the chain - - if (verifyLinks) { - const tombstone = refRoom.currentState.getStateEvents(_event2.EventType.RoomTombstone, ""); - - if (!tombstone || tombstone.getContent()['replacement_room'] !== refRoom.roomId) { - break; - } - } // Insert at the front because we're working backwards from the currentRoom - - - upgradeHistory.splice(0, 0, refRoom); - createEvent = refRoom.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); - } else { - // No further create events to look at + const before = this.findPredecessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor); + const after = this.findSuccessorRooms(currentRoom, verifyLinks, msc3946ProcessDynamicPredecessor); + return [...before, currentRoom, ...after]; + } + findPredecessorRooms(room, verifyLinks, msc3946ProcessDynamicPredecessor) { + const ret = []; + + // Work backwards from newer to older rooms + let predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId; + while (predecessorRoomId !== null) { + const predecessorRoom = this.getRoom(predecessorRoomId); + if (predecessorRoom === null) { break; } - } // Work forwards next, looking at tombstone events - + if (verifyLinks) { + const tombstone = predecessorRoom.currentState.getStateEvents(_event2.EventType.RoomTombstone, ""); + if (!tombstone || tombstone.getContent()["replacement_room"] !== room.roomId) { + break; + } + } - let tombstoneEvent = currentRoom.currentState.getStateEvents(_event2.EventType.RoomTombstone, ""); + // Insert at the front because we're working backwards from the currentRoom + ret.splice(0, 0, predecessorRoom); + room = predecessorRoom; + predecessorRoomId = room.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId; + } + return ret; + } + findSuccessorRooms(room, verifyLinks, msc3946ProcessDynamicPredecessor) { + const ret = []; + // Work forwards, looking at tombstone events + let tombstoneEvent = room.currentState.getStateEvents(_event2.EventType.RoomTombstone, ""); while (tombstoneEvent) { - const refRoom = this.getRoom(tombstoneEvent.getContent()['replacement_room']); - if (!refRoom) break; // end of the chain - - if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room + const successorRoom = this.getRoom(tombstoneEvent.getContent()["replacement_room"]); + if (!successorRoom) break; // end of the chain + if (successorRoom.roomId === room.roomId) break; // Tombstone is referencing its own room if (verifyLinks) { - createEvent = refRoom.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); - if (!createEvent || !createEvent.getContent()['predecessor']) break; - const predecessor = createEvent.getContent()['predecessor']; - if (predecessor['room_id'] !== currentRoom.roomId) break; - } // Push to the end because we're looking forwards - - - upgradeHistory.push(refRoom); - const roomIds = new Set(upgradeHistory.map(ref => ref.roomId)); + const predecessorRoomId = successorRoom.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId; + if (!predecessorRoomId || predecessorRoomId !== room.roomId) { + break; + } + } - if (roomIds.size < upgradeHistory.length) { + // Push to the end because we're looking forwards + ret.push(successorRoom); + const roomIds = new Set(ret.map(ref => ref.roomId)); + if (roomIds.size < ret.length) { // The last room added to the list introduced a previous roomId // To avoid recursion, return the last rooms - 1 - return upgradeHistory.slice(0, upgradeHistory.length - 1); - } // Set the current room to the reference room so we know where we're at - + return ret.slice(0, ret.length - 1); + } - currentRoom = refRoom; - tombstoneEvent = currentRoom.currentState.getStateEvents(_event2.EventType.RoomTombstone, ""); + // Set the current room to the reference room so we know where we're at + room = successorRoom; + tombstoneEvent = room.currentState.getStateEvents(_event2.EventType.RoomTombstone, ""); } - - return upgradeHistory; + return ret; } + /** - * @param {string} roomId - * @param {string} userId - * @param {module:client.callback} callback Optional. - * @param {string} reason Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param reason - Optional. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ - - - invite(roomId, userId, callback, reason) { - return this.membershipChange(roomId, userId, "invite", reason, callback); + invite(roomId, userId, reason) { + return this.membershipChange(roomId, userId, "invite", reason); } + /** * Invite a user to a room based on their email address. - * @param {string} roomId The room to invite the user to. - * @param {string} email The email address to invite. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param roomId - The room to invite the user to. + * @param email - The email address to invite. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ - - - inviteByEmail(roomId, email, callback) { - return this.inviteByThreePid(roomId, "email", email, callback); + inviteByEmail(roomId, email) { + return this.inviteByThreePid(roomId, "email", email); } + /** * Invite a user to a room based on a third-party identifier. - * @param {string} roomId The room to invite the user to. - * @param {string} medium The medium to invite the user e.g. "email". - * @param {string} address The address for the specified medium. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param roomId - The room to invite the user to. + * @param medium - The medium to invite the user e.g. "email". + * @param address - The address for the specified medium. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ - - - async inviteByThreePid(roomId, medium, address, callback) { + async inviteByThreePid(roomId, medium, address) { const path = utils.encodeUri("/rooms/$roomId/invite", { $roomId: roomId }); const identityServerUrl = this.getIdentityServerUrl(true); - if (!identityServerUrl) { return Promise.reject(new _httpApi.MatrixError({ error: "No supplied identity server URL", errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM" })); } - const params = { id_server: identityServerUrl, medium: medium, address: address }; - if (this.identityServer?.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) { const identityAccessToken = await this.identityServer.getAccessToken(); - if (identityAccessToken) { - params['id_access_token'] = identityAccessToken; + params["id_access_token"] = identityAccessToken; } } - - return this.http.authedRequest(callback, _httpApi.Method.Post, path, undefined, params); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, params); } + /** - * @param {string} roomId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ - - - leave(roomId, callback) { - return this.membershipChange(roomId, undefined, "leave", undefined, callback); + leave(roomId) { + return this.membershipChange(roomId, undefined, "leave"); } + /** * Leaves all rooms in the chain of room upgrades based on the given room. By * default, this will leave all the previous and upgraded rooms, including the * given room. To only leave the given room and any previous rooms, keeping the * upgraded (modern) rooms untouched supply `false` to `includeFuture`. - * @param {string} roomId The room ID to start leaving at - * @param {boolean} includeFuture If true, the whole chain (past and future) of + * @param roomId - The room ID to start leaving at + * @param includeFuture - If true, the whole chain (past and future) of * upgraded rooms will be left. - * @return {Promise} Resolves when completed with an object keyed + * @returns Promise which resolves when completed with an object keyed * by room ID and value of the error encountered when leaving or null. */ - - leaveRoomChain(roomId, includeFuture = true) { const upgradeHistory = this.getRoomUpgradeHistory(roomId); let eligibleToLeave = upgradeHistory; - if (!includeFuture) { eligibleToLeave = []; - for (const room of upgradeHistory) { eligibleToLeave.push(room); - if (room.roomId === roomId) { break; } } } - const populationResults = {}; const promises = []; - const doLeave = roomId => { return this.leave(roomId).then(() => { - populationResults[roomId] = null; + delete populationResults[roomId]; }).catch(err => { + // suppress error populationResults[roomId] = err; - return null; // suppress error }); }; - for (const room of eligibleToLeave) { promises.push(doLeave(room.roomId)); } - return Promise.all(promises).then(() => populationResults); } + /** - * @param {string} roomId - * @param {string} userId - * @param {string} reason Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param reason - Optional. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - ban(roomId, userId, reason, callback) { - return this.membershipChange(roomId, userId, "ban", reason, callback); + ban(roomId, userId, reason) { + return this.membershipChange(roomId, userId, "ban", reason); } + /** - * @param {string} roomId - * @param {boolean} deleteRoom True to delete the room from the store on success. + * @param deleteRoom - True to delete the room from the store on success. * Default: true. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ - - - forget(roomId, deleteRoom, callback) { - if (deleteRoom === undefined) { - deleteRoom = true; - } - - const promise = this.membershipChange(roomId, undefined, "forget", undefined, callback); - + forget(roomId, deleteRoom = true) { + const promise = this.membershipChange(roomId, undefined, "forget"); if (!deleteRoom) { return promise; } - return promise.then(response => { this.store.removeRoom(roomId); this.emit(ClientEvent.DeleteRoom, roomId); return response; }); } + /** - * @param {string} roomId - * @param {string} userId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Object (currently empty) - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Object (currently empty) + * @returns Rejects: with an error response. */ - - - unban(roomId, userId, callback) { + unban(roomId, userId) { // unbanning != set their state to leave: this used to be // the case, but was then changed so that leaving was always // a revoking of privilege, otherwise two people racing to @@ -4147,19 +3651,15 @@ const data = { user_id: userId }; - return this.http.authedRequest(callback, _httpApi.Method.Post, path, undefined, data); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data); } + /** - * @param {string} roomId - * @param {string} userId - * @param {string} reason Optional. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param reason - Optional. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ - - - kick(roomId, userId, reason, callback) { + kick(roomId, userId, reason) { const path = utils.encodeUri("/rooms/$roomId/kick", { $roomId: roomId }); @@ -4167,167 +3667,132 @@ user_id: userId, reason: reason }; - return this.http.authedRequest(callback, _httpApi.Method.Post, path, undefined, data); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data); } - - membershipChange(roomId, userId, membership, reason, callback) { + membershipChange(roomId, userId, membership, reason) { // API returns an empty object - if (utils.isFunction(reason)) { - callback = reason; // legacy - - reason = undefined; - } - const path = utils.encodeUri("/rooms/$room_id/$membership", { $room_id: roomId, $membership: membership }); - return this.http.authedRequest(callback, _httpApi.Method.Post, path, undefined, { + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, { user_id: userId, // may be undefined e.g. on leave reason: reason }); } + /** * Obtain a dict of actions which should be performed for this event according * to the push rules for this user. Caches the dict on the event. - * @param {MatrixEvent} event The event to get push actions for. - * @param {boolean} forceRecalculate forces to recalculate actions for an event + * @param event - The event to get push actions for. + * @param forceRecalculate - forces to recalculate actions for an event * Useful when an event just got decrypted - * @return {module:pushprocessor~PushAction} A dict of actions to perform. + * @returns A dict of actions to perform. */ - - getPushActionsForEvent(event, forceRecalculate = false) { if (!event.getPushActions() || forceRecalculate) { event.setPushActions(this.pushProcessor.actionsForEvent(event)); } - return event.getPushActions(); } + /** - * @param {string} info The kind of info to set (e.g. 'avatar_url') - * @param {Object} data The JSON object to set. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param info - The kind of info to set (e.g. 'avatar_url') + * @param data - The JSON object to set. + * @returns + * @returns Rejects: with an error response. */ // eslint-disable-next-line camelcase - - setProfileInfo(info, data, callback) { + setProfileInfo(info, data) { const path = utils.encodeUri("/profile/$userId/$info", { $userId: this.credentials.userId, $info: info }); - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, data); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, data); } + /** - * @param {string} name - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ - - - async setDisplayName(name, callback) { + async setDisplayName(name) { const prom = await this.setProfileInfo("displayname", { displayname: name - }, callback); // XXX: synthesise a profile update for ourselves because Synapse is broken and won't - + }); + // XXX: synthesise a profile update for ourselves because Synapse is broken and won't const user = this.getUser(this.getUserId()); - if (user) { user.displayName = name; user.emit(_user.UserEvent.DisplayName, user.events.presence, user); } - return prom; } + /** - * @param {string} url - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {} an empty object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: `{}` an empty object. + * @returns Rejects: with an error response. */ - - - async setAvatarUrl(url, callback) { + async setAvatarUrl(url) { const prom = await this.setProfileInfo("avatar_url", { avatar_url: url - }, callback); // XXX: synthesise a profile update for ourselves because Synapse is broken and won't - + }); + // XXX: synthesise a profile update for ourselves because Synapse is broken and won't const user = this.getUser(this.getUserId()); - if (user) { user.avatarUrl = url; user.emit(_user.UserEvent.AvatarUrl, user.events.presence, user); } - return prom; } + /** * Turn an MXC URL into an HTTP one. This method is experimental and * may change. - * @param {string} mxcUrl The MXC URL - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param mxcUrl - The MXC URL + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * @param allowDirectLinks - If true, return any non-mxc URLs * directly. Fetching such URLs will leak information about the user to * anyone they share a room with. If false, will return null for such URLs. - * @return {?string} the avatar URL or null. + * @returns the avatar URL or null. */ - - mxcUrlToHttp(mxcUrl, width, height, resizeMethod, allowDirectLinks) { return (0, _contentRepo.getHttpUriForMxc)(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks); } + /** - * @param {Object} opts Options to apply - * @param {string} opts.presence One of "online", "offline" or "unavailable" - * @param {string} opts.status_msg The status message to attach. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param opts - Options to apply + * @returns Promise which resolves + * @returns Rejects: with an error response. * @throws If 'presence' isn't a valid presence enum value. */ - - - setPresence(opts, callback) { + async setPresence(opts) { const path = utils.encodeUri("/presence/$userId/status", { $userId: this.credentials.userId }); - - if (typeof opts === "string") { - opts = { - presence: opts - }; // legacy - } - const validStates = ["offline", "online", "unavailable"]; - if (validStates.indexOf(opts.presence) === -1) { throw new Error("Bad presence value: " + opts.presence); } - - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, opts); + await this.http.authedRequest(_httpApi.Method.Put, path, undefined, opts); } + /** - * @param {string} userId The user to get presence for - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: The presence state for this user. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param userId - The user to get presence for + * @returns Promise which resolves: The presence state for this user. + * @returns Rejects: with an error response. */ - - - getPresence(userId, callback) { + getPresence(userId) { const path = utils.encodeUri("/presence/$userId/status", { $userId: userId }); - return this.http.authedRequest(callback, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** * Retrieve older messages from the given room and put them in the timeline. * @@ -4336,102 +3801,73 @@ * will be a small delay before another request can be made (to prevent tight-looping * when there is no connection). * - * @param {Room} room The room to get older messages in. - * @param {number} limit Optional. The maximum number of previous events to + * @param room - The room to get older messages in. + * @param limit - Optional. The maximum number of previous events to * pull in. Default: 30. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Room. If you are at the beginning - * of the timeline, Room.oldState.paginationToken will be - * null. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Room. If you are at the beginning + * of the timeline, `Room.oldState.paginationToken` will be + * `null`. + * @returns Rejects: with an error response. */ - - - scrollback(room, limit = 30, callback) { - if (utils.isFunction(limit)) { - callback = limit; // legacy - - limit = undefined; - } - + scrollback(room, limit = 30) { let timeToWaitMs = 0; let info = this.ongoingScrollbacks[room.roomId] || {}; - if (info.promise) { return info.promise; } else if (info.errorTs) { const timeWaitedMs = Date.now() - info.errorTs; timeToWaitMs = Math.max(SCROLLBACK_DELAY_MS - timeWaitedMs, 0); } - if (room.oldState.paginationToken === null) { return Promise.resolve(room); // already at the start. - } // attempt to grab more events from the store first - - + } + // attempt to grab more events from the store first const numAdded = this.store.scrollback(room, limit).length; - if (numAdded === limit) { // store contained everything we needed. return Promise.resolve(room); - } // reduce the required number of events appropriately - - + } + // reduce the required number of events appropriately limit = limit - numAdded; - const prom = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { // wait for a time before doing this request // (which may be 0 in order not to special case the code paths) (0, utils.sleep)(timeToWaitMs).then(() => { return this.createMessagesRequest(room.roomId, room.oldState.paginationToken, limit, _eventTimeline.Direction.Backward); }).then(res => { const matrixEvents = res.chunk.map(this.getEventMapper()); - if (res.state) { const stateEvents = res.state.map(this.getEventMapper()); room.currentState.setUnknownStateEvents(stateEvents); } - const [timelineEvents, threadedEvents] = room.partitionThreadedEvents(matrixEvents); - this.processBeaconEvents(room, timelineEvents); + this.processAggregatedTimelineEvents(room, timelineEvents); room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline()); this.processThreadEvents(room, threadedEvents, true); - room.oldState.paginationToken = res.end; - + room.oldState.paginationToken = res.end ?? null; if (res.chunk.length === 0) { room.oldState.paginationToken = null; } - - this.store.storeEvents(room, matrixEvents, res.end, true); - this.ongoingScrollbacks[room.roomId] = null; - callback?.(null, room); + this.store.storeEvents(room, matrixEvents, res.end ?? null, true); + delete this.ongoingScrollbacks[room.roomId]; resolve(room); }).catch(err => { this.ongoingScrollbacks[room.roomId] = { errorTs: Date.now() }; - callback?.(err); reject(err); }); }); info = { - promise: prom, - errorTs: null + promise }; this.ongoingScrollbacks[room.roomId] = info; - return prom; + return promise; } - /** - * @param {object} [options] - * @param {boolean} options.preventReEmit don't re-emit events emitted on an event mapped by this mapper on the client - * @param {boolean} options.decrypt decrypt event proactively - * @param {boolean} options.toDevice the event is a to_device event - * @return {Function} - */ - - getEventMapper(options) { return (0, _eventMapper.eventMapperFor)(this, options || {}); } + /** * Get an EventTimeline for the given event * @@ -4440,90 +3876,61 @@ * made, and used to construct an EventTimeline. * If the event does not belong to this EventTimelineSet then undefined will be returned. * - * @param {EventTimelineSet} timelineSet The timelineSet to look for the event in, must be bound to a room - * @param {string} eventId The ID of the event to look for + * @param timelineSet - The timelineSet to look for the event in, must be bound to a room + * @param eventId - The ID of the event to look for * - * @return {Promise} Resolves: - * {@link module:models/event-timeline~EventTimeline} including the given event + * @returns Promise which resolves: + * {@link EventTimeline} including the given event */ - - async getEventTimeline(timelineSet, eventId) { // don't allow any timeline support unless it's been enabled. if (!this.timelineSupport) { throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable it."); } - + if (!timelineSet?.room) { + throw new Error("getEventTimeline only supports room timelines"); + } if (timelineSet.getTimelineForEvent(eventId)) { return timelineSet.getTimelineForEvent(eventId); } - + if (timelineSet.thread && this.supportsThreads()) { + return this.getThreadTimeline(timelineSet, eventId); + } const path = utils.encodeUri("/rooms/$roomId/context/$eventId", { $roomId: timelineSet.room.roomId, $eventId: eventId }); let params = undefined; - - if (this.clientOpts.lazyLoadMembers) { + if (this.clientOpts?.lazyLoadMembers) { params = { filter: JSON.stringify(_filter.Filter.LAZY_LOADING_MESSAGES_FILTER) }; - } // TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors. - - - const res = await this.http.authedRequest(undefined, _httpApi.Method.Get, path, params); + } + // TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors. + const res = await this.http.authedRequest(_httpApi.Method.Get, path, params); if (!res.event) { throw new Error("'event' not in '/context' result - homeserver too old?"); - } // by the time the request completes, the event might have ended up in the timeline. - + } + // by the time the request completes, the event might have ended up in the timeline. if (timelineSet.getTimelineForEvent(eventId)) { return timelineSet.getTimelineForEvent(eventId); } - const mapper = this.getEventMapper(); const event = mapper(res.event); - const events = [// Order events from most recent to oldest (reverse-chronological). + if (event.isRelation(_thread.THREAD_RELATION_TYPE.name)) { + _logger.logger.warn("Tried loading a regular timeline at the position of a thread event"); + return undefined; + } + const events = [ + // Order events from most recent to oldest (reverse-chronological). // We start with the last event, since that's the point at which we have known state. // events_after is already backwards; events_before is forwards. ...res.events_after.reverse().map(mapper), event, ...res.events_before.map(mapper)]; - if (this.supportsExperimentalThreads()) { - if (!timelineSet.canContain(event)) { - return undefined; - } // Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only - // functions contiguously, so we have to jump through some hoops to get our target event in it. - // XXX: workaround for https://github.com/vector-im/element-meta/issues/150 - - - if (_thread.Thread.hasServerSideSupport && timelineSet.thread) { - const thread = timelineSet.thread; - const opts = { - direction: _eventTimeline.Direction.Backward, - limit: 50 - }; - await thread.fetchInitialEvents(); - let nextBatch = thread.liveTimeline.getPaginationToken(_eventTimeline.Direction.Backward); // Fetch events until we find the one we were asked for, or we run out of pages - - while (!thread.findEventById(eventId)) { - if (nextBatch) { - opts.from = nextBatch; - } - - ({ - nextBatch - } = await thread.fetchEvents(opts)); - if (!nextBatch) break; - } - - return thread.liveTimeline; - } - } // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries. - - + // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries. let timeline = timelineSet.getTimelineForEvent(events[0].getId()); - if (timeline) { timeline.getState(_eventTimeline.EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper)); } else { @@ -4531,71 +3938,194 @@ timeline.initialiseState(res.state.map(mapper)); timeline.getState(_eventTimeline.EventTimeline.FORWARDS).paginationToken = res.end; } - const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(events); - timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start); // The target event is not in a thread but process the contextual events, so we can show any threads around it. - + timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start); + // The target event is not in a thread but process the contextual events, so we can show any threads around it. this.processThreadEvents(timelineSet.room, threadedEvents, true); - this.processBeaconEvents(timelineSet.room, timelineEvents); // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring + this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents); + + // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring // timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up // anywhere, if it was later redacted, so we just return the timeline we first thought of. + return timelineSet.getTimelineForEvent(eventId) ?? timelineSet.room.findThreadForEvent(event)?.liveTimeline ?? + // for Threads degraded support + timeline; + } + async getThreadTimeline(timelineSet, eventId) { + if (!this.supportsThreads()) { + throw new Error("could not get thread timeline: no client support"); + } + if (!timelineSet.room) { + throw new Error("could not get thread timeline: not a room timeline"); + } + if (!timelineSet.thread) { + throw new Error("could not get thread timeline: not a thread timeline"); + } + const path = utils.encodeUri("/rooms/$roomId/context/$eventId", { + $roomId: timelineSet.room.roomId, + $eventId: eventId + }); + const params = { + limit: "0" + }; + if (this.clientOpts?.lazyLoadMembers) { + params.filter = JSON.stringify(_filter.Filter.LAZY_LOADING_MESSAGES_FILTER); + } - return timelineSet.getTimelineForEvent(eventId) ?? timelineSet.room.findThreadForEvent(event)?.liveTimeline // for Threads degraded support - ?? timeline; + // TODO: we should implement a backoff (as per scrollback()) to deal more nicely with HTTP errors. + const res = await this.http.authedRequest(_httpApi.Method.Get, path, params); + const mapper = this.getEventMapper(); + const event = mapper(res.event); + if (!timelineSet.canContain(event)) { + return undefined; + } + if (_thread.Thread.hasServerSideSupport) { + if (_thread.Thread.hasServerSideFwdPaginationSupport) { + if (!timelineSet.thread) { + throw new Error("could not get thread timeline: not a thread timeline"); + } + const thread = timelineSet.thread; + const resOlder = await this.fetchRelations(timelineSet.room.roomId, thread.id, _thread.THREAD_RELATION_TYPE.name, null, { + dir: _eventTimeline.Direction.Backward, + from: res.start + }); + const resNewer = await this.fetchRelations(timelineSet.room.roomId, thread.id, _thread.THREAD_RELATION_TYPE.name, null, { + dir: _eventTimeline.Direction.Forward, + from: res.end + }); + const events = [ + // Order events from most recent to oldest (reverse-chronological). + // We start with the last event, since that's the point at which we have known state. + // events_after is already backwards; events_before is forwards. + ...resNewer.chunk.reverse().map(mapper), event, ...resOlder.chunk.map(mapper)]; + for (const event of events) { + await timelineSet.thread?.processEvent(event); + } + + // Here we handle non-thread timelines only, but still process any thread events to populate thread summaries. + let timeline = timelineSet.getTimelineForEvent(event.getId()); + if (timeline) { + timeline.getState(_eventTimeline.EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper)); + } else { + timeline = timelineSet.addTimeline(); + timeline.initialiseState(res.state.map(mapper)); + } + timelineSet.addEventsToTimeline(events, true, timeline, resNewer.next_batch); + if (!resOlder.next_batch) { + const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id); + timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null); + } + timeline.setPaginationToken(resOlder.next_batch ?? null, _eventTimeline.Direction.Backward); + timeline.setPaginationToken(resNewer.next_batch ?? null, _eventTimeline.Direction.Forward); + this.processAggregatedTimelineEvents(timelineSet.room, events); + + // There is no guarantee that the event ended up in "timeline" (we might have switched to a neighbouring + // timeline) - so check the room's index again. On the other hand, there's no guarantee the event ended up + // anywhere, if it was later redacted, so we just return the timeline we first thought of. + return timelineSet.getTimelineForEvent(eventId) ?? timeline; + } else { + // Where the event is a thread reply (not a root) and running in MSC-enabled mode the Thread timeline only + // functions contiguously, so we have to jump through some hoops to get our target event in it. + // XXX: workaround for https://github.com/vector-im/element-meta/issues/150 + + const thread = timelineSet.thread; + const resOlder = await this.fetchRelations(timelineSet.room.roomId, thread.id, _thread.THREAD_RELATION_TYPE.name, null, { + dir: _eventTimeline.Direction.Backward, + from: res.start + }); + const eventsNewer = []; + let nextBatch = res.end; + while (nextBatch) { + const resNewer = await this.fetchRelations(timelineSet.room.roomId, thread.id, _thread.THREAD_RELATION_TYPE.name, null, { + dir: _eventTimeline.Direction.Forward, + from: nextBatch + }); + nextBatch = resNewer.next_batch ?? null; + eventsNewer.push(...resNewer.chunk); + } + const events = [ + // Order events from most recent to oldest (reverse-chronological). + // We start with the last event, since that's the point at which we have known state. + // events_after is already backwards; events_before is forwards. + ...eventsNewer.reverse().map(mapper), event, ...resOlder.chunk.map(mapper)]; + for (const event of events) { + await timelineSet.thread?.processEvent(event); + } + + // Here we handle non-thread timelines only, but still process any thread events to populate thread + // summaries. + const timeline = timelineSet.getLiveTimeline(); + timeline.getState(_eventTimeline.EventTimeline.BACKWARDS).setUnknownStateEvents(res.state.map(mapper)); + timelineSet.addEventsToTimeline(events, true, timeline, null); + if (!resOlder.next_batch) { + const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id); + timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null); + } + timeline.setPaginationToken(resOlder.next_batch ?? null, _eventTimeline.Direction.Backward); + timeline.setPaginationToken(null, _eventTimeline.Direction.Forward); + this.processAggregatedTimelineEvents(timelineSet.room, events); + return timeline; + } + } } + /** * Get an EventTimeline for the latest events in the room. This will just * call `/messages` to get the latest message in the room, then use * `client.getEventTimeline(...)` to construct a new timeline from it. * - * @param {EventTimelineSet} timelineSet The timelineSet to find or add the timeline to + * @param timelineSet - The timelineSet to find or add the timeline to * - * @return {Promise} Resolves: - * {@link module:models/event-timeline~EventTimeline} timeline with the latest events in the room + * @returns Promise which resolves: + * {@link EventTimeline} timeline with the latest events in the room */ - - async getLatestTimeline(timelineSet) { // don't allow any timeline support unless it's been enabled. if (!this.timelineSupport) { throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable it."); } - - const messagesPath = utils.encodeUri("/rooms/$roomId/messages", { - $roomId: timelineSet.room.roomId - }); - const params = { - dir: 'b' - }; - - if (this.clientOpts.lazyLoadMembers) { - params.filter = JSON.stringify(_filter.Filter.LAZY_LOADING_MESSAGES_FILTER); + if (!timelineSet.room) { + throw new Error("getLatestTimeline only supports room timelines"); + } + let event; + if (timelineSet.threadListType !== null) { + const res = await this.createThreadListMessagesRequest(timelineSet.room.roomId, null, 1, _eventTimeline.Direction.Backward, timelineSet.threadListType, timelineSet.getFilter()); + event = res.chunk?.[0]; + } else if (timelineSet.thread && _thread.Thread.hasServerSideSupport) { + const res = await this.fetchRelations(timelineSet.room.roomId, timelineSet.thread.id, _thread.THREAD_RELATION_TYPE.name, null, { + dir: _eventTimeline.Direction.Backward, + limit: 1 + }); + event = res.chunk?.[0]; + } else { + const messagesPath = utils.encodeUri("/rooms/$roomId/messages", { + $roomId: timelineSet.room.roomId + }); + const params = { + dir: "b" + }; + if (this.clientOpts?.lazyLoadMembers) { + params.filter = JSON.stringify(_filter.Filter.LAZY_LOADING_MESSAGES_FILTER); + } + const res = await this.http.authedRequest(_httpApi.Method.Get, messagesPath, params); + event = res.chunk?.[0]; } - - const res = await this.http.authedRequest(undefined, _httpApi.Method.Get, messagesPath, params); - const event = res.chunk?.[0]; - if (!event) { - throw new Error("No message returned from /messages when trying to construct getLatestTimeline"); + throw new Error("No message returned when trying to construct getLatestTimeline"); } - return this.getEventTimeline(timelineSet, event.event_id); } + /** * Makes a request to /messages with the appropriate lazy loading filter set. * XXX: if we do get rid of scrollback (as it's not used at the moment), * we could inline this method again in paginateEventTimeline as that would * then be the only call-site - * @param {string} roomId - * @param {string} fromToken - * @param {number} limit the maximum amount of events the retrieve - * @param {string} dir 'f' or 'b' - * @param {Filter} timelineFilter the timeline filter to pass - * @return {Promise} + * @param limit - the maximum amount of events the retrieve + * @param dir - 'f' or 'b' + * @param timelineFilter - the timeline filter to pass */ // XXX: Intended private, used in code. - - createMessagesRequest(roomId, fromToken, limit = 30, dir, timelineFilter) { const path = utils.encodeUri("/rooms/$roomId/messages", { $roomId: roomId @@ -4604,176 +4134,275 @@ limit: limit.toString(), dir: dir }; - if (fromToken) { params.from = fromToken; } - let filter = null; - - if (this.clientOpts.lazyLoadMembers) { + if (this.clientOpts?.lazyLoadMembers) { // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, // so the timelineFilter doesn't get written into it below filter = Object.assign({}, _filter.Filter.LAZY_LOADING_MESSAGES_FILTER); } - if (timelineFilter) { // XXX: it's horrific that /messages' filter parameter doesn't match // /sync's one - see https://matrix.org/jira/browse/SPEC-451 filter = filter || {}; Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()?.toJSON()); } - if (filter) { params.filter = JSON.stringify(filter); } + return this.http.authedRequest(_httpApi.Method.Get, path, params); + } - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, params); + /** + * Makes a request to /messages with the appropriate lazy loading filter set. + * XXX: if we do get rid of scrollback (as it's not used at the moment), + * we could inline this method again in paginateEventTimeline as that would + * then be the only call-site + * @param limit - the maximum amount of events the retrieve + * @param dir - 'f' or 'b' + * @param timelineFilter - the timeline filter to pass + */ + // XXX: Intended private, used by room.fetchRoomThreads + createThreadListMessagesRequest(roomId, fromToken, limit = 30, dir = _eventTimeline.Direction.Backward, threadListType = _thread.ThreadFilterType.All, timelineFilter) { + const path = utils.encodeUri("/rooms/$roomId/threads", { + $roomId: roomId + }); + const params = { + limit: limit.toString(), + dir: dir, + include: (0, _thread.threadFilterTypeToFilter)(threadListType) + }; + if (fromToken) { + params.from = fromToken; + } + let filter = {}; + if (this.clientOpts?.lazyLoadMembers) { + // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, + // so the timelineFilter doesn't get written into it below + filter = _objectSpread({}, _filter.Filter.LAZY_LOADING_MESSAGES_FILTER); + } + if (timelineFilter) { + // XXX: it's horrific that /messages' filter parameter doesn't match + // /sync's one - see https://matrix.org/jira/browse/SPEC-451 + filter = _objectSpread(_objectSpread({}, filter), timelineFilter.getRoomTimelineFilterComponent()?.toJSON()); + } + if (Object.keys(filter).length) { + params.filter = JSON.stringify(filter); + } + const opts = { + prefix: _thread.Thread.hasServerSideListSupport === _thread.FeatureSupport.Stable ? "/_matrix/client/v1" : "/_matrix/client/unstable/org.matrix.msc3856" + }; + return this.http.authedRequest(_httpApi.Method.Get, path, params, undefined, opts).then(res => _objectSpread(_objectSpread({}, res), {}, { + chunk: res.chunk?.reverse(), + start: res.prev_batch, + end: res.next_batch + })); } + /** * Take an EventTimeline, and back/forward-fill results. * - * @param {module:models/event-timeline~EventTimeline} eventTimeline timeline - * object to be updated - * @param {Object} [opts] - * @param {boolean} [opts.backwards = false] true to fill backwards, - * false to go forwards - * @param {number} [opts.limit = 30] number of events to request + * @param eventTimeline - timeline object to be updated * - * @return {Promise} Resolves to a boolean: false if there are no + * @returns Promise which resolves to a boolean: false if there are no * events and we reached either end of the timeline; else true. */ - - paginateEventTimeline(eventTimeline, opts) { - const isNotifTimeline = eventTimeline.getTimelineSet() === this.notifTimelineSet; // TODO: we should implement a backoff (as per scrollback()) to deal more - // nicely with HTTP errors. + const isNotifTimeline = eventTimeline.getTimelineSet() === this.notifTimelineSet; + const room = this.getRoom(eventTimeline.getRoomId()); + const threadListType = eventTimeline.getTimelineSet().threadListType; + const thread = eventTimeline.getTimelineSet().thread; + // TODO: we should implement a backoff (as per scrollback()) to deal more + // nicely with HTTP errors. opts = opts || {}; const backwards = opts.backwards || false; - if (isNotifTimeline) { if (!backwards) { throw new Error("paginateNotifTimeline can only paginate backwards"); } } - const dir = backwards ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS; const token = eventTimeline.getPaginationToken(dir); const pendingRequest = eventTimeline.paginationRequests[dir]; - if (pendingRequest) { // already a request in progress - return the existing promise return pendingRequest; } - let path; let params; let promise; - if (isNotifTimeline) { path = "/notifications"; params = { limit: (opts.limit ?? 30).toString(), - only: 'highlight' + only: "highlight" }; - - if (token !== "end") { + if (token && token !== "end") { params.from = token; } - - promise = this.http.authedRequest(undefined, _httpApi.Method.Get, path, params).then(async res => { + promise = this.http.authedRequest(_httpApi.Method.Get, path, params).then(async res => { const token = res.next_token; const matrixEvents = []; - + res.notifications = res.notifications.filter(utils.noUnsafeEventProps); for (let i = 0; i < res.notifications.length; i++) { const notification = res.notifications[i]; const event = this.getEventMapper()(notification.event); event.setPushActions(_pushprocessor.PushProcessor.actionListToActionsObject(notification.actions)); event.event.room_id = notification.room_id; // XXX: gutwrenching - matrixEvents[i] = event; - } // No need to partition events for threads here, everything lives - // in the notification timeline set - + } + // No need to partition events for threads here, everything lives + // in the notification timeline set const timelineSet = eventTimeline.getTimelineSet(); timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); - this.processBeaconEvents(timelineSet.room, matrixEvents); // if we've hit the end of the timeline, we need to stop trying to + this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents); + + // if we've hit the end of the timeline, we need to stop trying to // paginate. We need to keep the 'forwards' token though, to make sure // we can recover from gappy syncs. - if (backwards && !res.next_token) { eventTimeline.setPaginationToken(null, dir); } - - return res.next_token ? true : false; + return Boolean(res.next_token); }).finally(() => { eventTimeline.paginationRequests[dir] = null; }); eventTimeline.paginationRequests[dir] = promise; - } else { - const room = this.getRoom(eventTimeline.getRoomId()); + } else if (threadListType !== null) { + if (!room) { + throw new Error("Unknown room " + eventTimeline.getRoomId()); + } + if (!_thread.Thread.hasServerSideFwdPaginationSupport && dir === _eventTimeline.Direction.Forward) { + throw new Error("Cannot paginate threads forwards without server-side support for MSC 3715"); + } + promise = this.createThreadListMessagesRequest(eventTimeline.getRoomId(), token, opts.limit, dir, threadListType, eventTimeline.getFilter()).then(res => { + if (res.state) { + const roomState = eventTimeline.getState(dir); + const stateEvents = res.state.filter(utils.noUnsafeEventProps).map(this.getEventMapper()); + roomState.setUnknownStateEvents(stateEvents); + } + const token = res.end; + const matrixEvents = res.chunk.filter(utils.noUnsafeEventProps).map(this.getEventMapper()); + const timelineSet = eventTimeline.getTimelineSet(); + timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); + this.processAggregatedTimelineEvents(room, matrixEvents); + this.processThreadRoots(room, matrixEvents, backwards); + // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + if (backwards && res.end == res.start) { + eventTimeline.setPaginationToken(null, dir); + } + return res.end !== res.start; + }).finally(() => { + eventTimeline.paginationRequests[dir] = null; + }); + eventTimeline.paginationRequests[dir] = promise; + } else if (thread) { + const room = this.getRoom(eventTimeline.getRoomId() ?? undefined); if (!room) { throw new Error("Unknown room " + eventTimeline.getRoomId()); } + promise = this.fetchRelations(eventTimeline.getRoomId() ?? "", thread.id, _thread.THREAD_RELATION_TYPE.name, null, { + dir, + limit: opts.limit, + from: token ?? undefined + }).then(async res => { + const mapper = this.getEventMapper(); + const matrixEvents = res.chunk.filter(utils.noUnsafeEventProps).map(mapper); + + // Process latest events first + for (const event of matrixEvents.slice().reverse()) { + await thread?.processEvent(event); + const sender = event.getSender(); + if (!backwards || thread?.getEventReadUpTo(sender) === null) { + room.addLocalEchoReceipt(sender, event, _read_receipts.ReceiptType.Read); + } + } + const newToken = res.next_batch; + const timelineSet = eventTimeline.getTimelineSet(); + timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken ?? null); + if (!newToken && backwards) { + const originalEvent = await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id); + timelineSet.addEventsToTimeline([mapper(originalEvent)], true, eventTimeline, null); + } + this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents); + // if we've hit the end of the timeline, we need to stop trying to + // paginate. We need to keep the 'forwards' token though, to make sure + // we can recover from gappy syncs. + if (backwards && !newToken) { + eventTimeline.setPaginationToken(null, dir); + } + return Boolean(newToken); + }).finally(() => { + eventTimeline.paginationRequests[dir] = null; + }); + eventTimeline.paginationRequests[dir] = promise; + } else { + if (!room) { + throw new Error("Unknown room " + eventTimeline.getRoomId()); + } promise = this.createMessagesRequest(eventTimeline.getRoomId(), token, opts.limit, dir, eventTimeline.getFilter()).then(res => { if (res.state) { const roomState = eventTimeline.getState(dir); - const stateEvents = res.state.map(this.getEventMapper()); + const stateEvents = res.state.filter(utils.noUnsafeEventProps).map(this.getEventMapper()); roomState.setUnknownStateEvents(stateEvents); } - const token = res.end; - const matrixEvents = res.chunk.map(this.getEventMapper()); + const matrixEvents = res.chunk.filter(utils.noUnsafeEventProps).map(this.getEventMapper()); const timelineSet = eventTimeline.getTimelineSet(); - const [timelineEvents, threadedEvents] = timelineSet.room.partitionThreadedEvents(matrixEvents); + const [timelineEvents] = room.partitionThreadedEvents(matrixEvents); timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token); - this.processBeaconEvents(timelineSet.room, timelineEvents); - this.processThreadEvents(room, threadedEvents, backwards); // if we've hit the end of the timeline, we need to stop trying to + this.processAggregatedTimelineEvents(room, timelineEvents); + this.processThreadRoots(room, timelineEvents.filter(it => it.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name)), false); + const atEnd = res.end === undefined || res.end === res.start; + + // if we've hit the end of the timeline, we need to stop trying to // paginate. We need to keep the 'forwards' token though, to make sure // we can recover from gappy syncs. - - if (backwards && res.end == res.start) { + if (backwards && atEnd) { eventTimeline.setPaginationToken(null, dir); } - - return res.end != res.start; + return !atEnd; }).finally(() => { eventTimeline.paginationRequests[dir] = null; }); eventTimeline.paginationRequests[dir] = promise; } - return promise; } + /** * Reset the notifTimelineSet entirely, paginating in some historical notifs as * a starting point for subsequent pagination. */ - - resetNotifTimelineSet() { if (!this.notifTimelineSet) { return; - } // FIXME: This thing is a total hack, and results in duplicate events being + } + + // FIXME: This thing is a total hack, and results in duplicate events being // added to the timeline both from /sync and /notifications, and lots of // slow and wasteful processing and pagination. The correct solution is to // extend /messages or /search or something to filter on notifications. + // use the fictitious token 'end'. in practice we would ideally give it // the oldest backwards pagination token from /sync, but /sync doesn't // know about /notifications, so we have no choice but to start paginating // from the current point in time. This may well overlap with historical // notifs which are then inserted into the timeline by /sync responses. + this.notifTimelineSet.resetLiveTimeline("end"); - - this.notifTimelineSet.resetLiveTimeline('end', null); // we could try to paginate a single event at this point in order to get + // we could try to paginate a single event at this point in order to get // a more valid pagination token, but it just ends up with an out of order // timeline. given what a mess this is and given we're going to have duplicate // events anyway, just leave it with the dummy token for now. - /* this.paginateNotifTimeline(this._notifTimelineSet.getLiveTimeline(), { backwards: true, @@ -4781,63 +4410,50 @@ }); */ } + /** * Peek into a room and receive updates about the room. This only works if the * history visibility for the room is world_readable. - * @param {String} roomId The room to attempt to peek into. - * @return {Promise} Resolves: Room object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param roomId - The room to attempt to peek into. + * @returns Promise which resolves: Room object + * @returns Rejects: with an error response. */ - - peekInRoom(roomId) { - if (this.peekSync) { - this.peekSync.stopPeeking(); - } - - this.peekSync = new _sync.SyncApi(this, this.clientOpts); + this.peekSync?.stopPeeking(); + this.peekSync = new _sync.SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); return this.peekSync.peek(roomId); } + /** * Stop any ongoing room peeking. */ - - stopPeeking() { if (this.peekSync) { this.peekSync.stopPeeking(); this.peekSync = null; } } + /** * Set r/w flags for guest access in a room. - * @param {string} roomId The room to configure guest access in. - * @param {Object} opts Options - * @param {boolean} opts.allowJoin True to allow guests to join this room. This - * implicitly gives guests write access. If false or not given, guests are - * explicitly forbidden from joining the room. - * @param {boolean} opts.allowRead True to set history visibility to - * be world_readable. This gives guests read access *from this point forward*. - * If false or not given, history visibility is not modified. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param roomId - The room to configure guest access in. + * @param opts - Options + * @returns Promise which resolves + * @returns Rejects: with an error response. */ - - setGuestAccess(roomId, opts) { const writePromise = this.sendStateEvent(roomId, _event2.EventType.RoomGuestAccess, { guest_access: opts.allowJoin ? "can_join" : "forbidden" }, ""); let readPromise = Promise.resolve(undefined); - if (opts.allowRead) { readPromise = this.sendStateEvent(roomId, _event2.EventType.RoomHistoryVisibility, { history_visibility: "world_readable" }, ""); } - return Promise.all([readPromise, writePromise]).then(); // .then() to hide results for contract } + /** * Requests an email verification token for the purposes of registration. * This API requests a token from the homeserver. @@ -4845,14 +4461,12 @@ * the server requires the id_server parameter to be provided. * * Parameters and return value are as for requestEmailToken - * @param {string} email As requestEmailToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken - */ - - + * @param email - As requestEmailToken + * @param clientSecret - As requestEmailToken + * @param sendAttempt - As requestEmailToken + * @param nextLink - As requestEmailToken + * @returns Promise which resolves: As requestEmailToken + */ requestRegisterEmailToken(email, clientSecret, sendAttempt, nextLink) { return this.requestTokenFromEndpoint("/register/email/requestToken", { email: email, @@ -4861,22 +4475,21 @@ next_link: nextLink }); } + /** * Requests a text message verification token for the purposes of registration. * This API requests a token from the homeserver. * The doesServerRequireIdServerParam() method can be used to determine if * the server requires the id_server parameter to be provided. * - * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in which + * @param phoneCountry - The ISO 3166-1 alpha-2 code for the country in which * phoneNumber should be parsed relative to. - * @param {string} phoneNumber The phone number, in national or international format - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken + * @param phoneNumber - The phone number, in national or international format + * @param clientSecret - As requestEmailToken + * @param sendAttempt - As requestEmailToken + * @param nextLink - As requestEmailToken + * @returns Promise which resolves: As requestEmailToken */ - - requestRegisterMsisdnToken(phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) { return this.requestTokenFromEndpoint("/register/msisdn/requestToken", { country: phoneCountry, @@ -4886,6 +4499,7 @@ next_link: nextLink }); } + /** * Requests an email verification token for the purposes of adding a * third party identifier to an account. @@ -4897,14 +4511,12 @@ * it will either send an email to the address informing them of this * or return M_THREEPID_IN_USE (which one is up to the homeserver). * - * @param {string} email As requestEmailToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken + * @param email - As requestEmailToken + * @param clientSecret - As requestEmailToken + * @param sendAttempt - As requestEmailToken + * @param nextLink - As requestEmailToken + * @returns Promise which resolves: As requestEmailToken */ - - requestAdd3pidEmailToken(email, clientSecret, sendAttempt, nextLink) { return this.requestTokenFromEndpoint("/account/3pid/email/requestToken", { email: email, @@ -4913,6 +4525,7 @@ next_link: nextLink }); } + /** * Requests a text message verification token for the purposes of adding a * third party identifier to an account. @@ -4920,15 +4533,13 @@ * adding specific behaviour for the addition of phone numbers to an * account, as requestAdd3pidEmailToken. * - * @param {string} phoneCountry As requestRegisterMsisdnToken - * @param {string} phoneNumber As requestRegisterMsisdnToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken + * @param phoneCountry - As requestRegisterMsisdnToken + * @param phoneNumber - As requestRegisterMsisdnToken + * @param clientSecret - As requestEmailToken + * @param sendAttempt - As requestEmailToken + * @param nextLink - As requestEmailToken + * @returns Promise which resolves: As requestEmailToken */ - - requestAdd3pidMsisdnToken(phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) { return this.requestTokenFromEndpoint("/account/3pid/msisdn/requestToken", { country: phoneCountry, @@ -4938,6 +4549,7 @@ next_link: nextLink }); } + /** * Requests an email verification token for the purposes of resetting * the password on an account. @@ -4950,15 +4562,12 @@ * requestEmailToken calls the equivalent API directly on the identity server, * therefore bypassing the password reset specific logic. * - * @param {string} email As requestEmailToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @param {module:client.callback} callback Optional. As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken + * @param email - As requestEmailToken + * @param clientSecret - As requestEmailToken + * @param sendAttempt - As requestEmailToken + * @param nextLink - As requestEmailToken + * @returns Promise which resolves: As requestEmailToken */ - - requestPasswordEmailToken(email, clientSecret, sendAttempt, nextLink) { return this.requestTokenFromEndpoint("/account/password/email/requestToken", { email: email, @@ -4967,21 +4576,20 @@ next_link: nextLink }); } + /** * Requests a text message verification token for the purposes of resetting * the password on an account. * This API proxies the identity server /validate/email/requestToken API, * adding specific behaviour for the password resetting, as requestPasswordEmailToken. * - * @param {string} phoneCountry As requestRegisterMsisdnToken - * @param {string} phoneNumber As requestRegisterMsisdnToken - * @param {string} clientSecret As requestEmailToken - * @param {number} sendAttempt As requestEmailToken - * @param {string} nextLink As requestEmailToken - * @return {Promise} Resolves: As requestEmailToken + * @param phoneCountry - As requestRegisterMsisdnToken + * @param phoneNumber - As requestRegisterMsisdnToken + * @param clientSecret - As requestEmailToken + * @param sendAttempt - As requestEmailToken + * @param nextLink - As requestEmailToken + * @returns Promise which resolves: As requestEmailToken */ - - requestPasswordMsisdnToken(phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink) { return this.requestTokenFromEndpoint("/account/password/msisdn/requestToken", { country: phoneCountry, @@ -4991,83 +4599,67 @@ next_link: nextLink }); } + /** * Internal utility function for requesting validation tokens from usage-specific * requestToken endpoints. * - * @param {string} endpoint The endpoint to send the request to - * @param {object} params Parameters for the POST request - * @return {Promise} Resolves: As requestEmailToken + * @param endpoint - The endpoint to send the request to + * @param params - Parameters for the POST request + * @returns Promise which resolves: As requestEmailToken */ - - async requestTokenFromEndpoint(endpoint, params) { - const postParams = Object.assign({}, params); // If the HS supports separate add and bind, then requestToken endpoints - // don't need an IS as they are all validated by the HS directly. + const postParams = Object.assign({}, params); + // If the HS supports separate add and bind, then requestToken endpoints + // don't need an IS as they are all validated by the HS directly. if (!(await this.doesServerSupportSeparateAddAndBind()) && this.idBaseUrl) { const idServerUrl = new URL(this.idBaseUrl); postParams.id_server = idServerUrl.host; - if (this.identityServer?.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) { const identityAccessToken = await this.identityServer.getAccessToken(); - if (identityAccessToken) { postParams.id_access_token = identityAccessToken; } } } - - return this.http.request(undefined, _httpApi.Method.Post, endpoint, undefined, postParams); + return this.http.request(_httpApi.Method.Post, endpoint, undefined, postParams); } + /** * Get the room-kind push rule associated with a room. - * @param {string} scope "global" or device-specific. - * @param {string} roomId the id of the room. - * @return {object} the rule or undefined. + * @param scope - "global" or device-specific. + * @param roomId - the id of the room. + * @returns the rule or undefined. */ - - getRoomPushRule(scope, roomId) { // There can be only room-kind push rule per room // and its id is the room id. if (this.pushRules) { - if (!this.pushRules[scope] || !this.pushRules[scope].room) { - return; - } - - for (let i = 0; i < this.pushRules[scope].room.length; i++) { - const rule = this.pushRules[scope].room[i]; - - if (rule.rule_id === roomId) { - return rule; - } - } + return this.pushRules[scope]?.room?.find(rule => rule.rule_id === roomId); } else { throw new Error("SyncApi.sync() must be done before accessing to push rules."); } } + /** * Set a room-kind muting push rule in a room. * The operation also updates MatrixClient.pushRules at the end. - * @param {string} scope "global" or device-specific. - * @param {string} roomId the id of the room. - * @param {boolean} mute the mute state. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param scope - "global" or device-specific. + * @param roomId - the id of the room. + * @param mute - the mute state. + * @returns Promise which resolves: result object + * @returns Rejects: with an error response. */ - - setRoomMutePushRule(scope, roomId, mute) { let promise; - let hasDontNotifyRule = false; // Get the existing room-kind push rule if any + let hasDontNotifyRule = false; + // Get the existing room-kind push rule if any const roomPushRule = this.getRoomPushRule(scope, roomId); - - if (roomPushRule?.actions.includes(_matrix.PushRuleActionName.DontNotify)) { + if (roomPushRule?.actions.includes(_PushRules.PushRuleActionName.DontNotify)) { hasDontNotifyRule = true; } - if (!mute) { // Remove the rule only if it is a muting rule if (hasDontNotifyRule) { @@ -5076,7 +4668,7 @@ } else { if (!roomPushRule) { promise = this.addPushRule(scope, _PushRules.PushRuleKind.RoomSpecific, roomId, { - actions: [_matrix.PushRuleActionName.DontNotify] + actions: [_PushRules.PushRuleActionName.DontNotify] }); } else if (!hasDontNotifyRule) { // Remove the existing one before setting the mute push rule @@ -5084,7 +4676,7 @@ const deferred = utils.defer(); this.deletePushRule(scope, _PushRules.PushRuleKind.RoomSpecific, roomPushRule.rule_id).then(() => { this.addPushRule(scope, _PushRules.PushRuleKind.RoomSpecific, roomId, { - actions: [_matrix.PushRuleActionName.DontNotify] + actions: [_PushRules.PushRuleActionName.DontNotify] }).then(() => { deferred.resolve(); }).catch(err => { @@ -5096,7 +4688,6 @@ promise = deferred.promise; } } - if (promise) { return new Promise((resolve, reject) => { // Update this.pushRules when the operation completes @@ -5120,48 +4711,40 @@ }); } } - - searchMessageText(opts, callback) { + searchMessageText(opts) { const roomEvents = { search_term: opts.query }; - - if ('keys' in opts) { + if ("keys" in opts) { roomEvents.keys = opts.keys; } - return this.search({ body: { search_categories: { room_events: roomEvents } } - }, callback); + }); } + /** * Perform a server-side search for room events. * * The returned promise resolves to an object containing the fields: * - * * {number} count: estimate of the number of results - * * {string} next_batch: token for back-pagination; if undefined, there are - * no more results - * * {Array} highlights: a list of words to highlight from the stemming - * algorithm - * * {Array} results: a list of results - * - * Each entry in the results list is a {module:models/search-result.SearchResult}. - * - * @param {Object} opts - * @param {string} opts.term the term to search for - * @param {Object} opts.filter a JSON filter object to pass in the request - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * * count: estimate of the number of results + * * next_batch: token for back-pagination; if undefined, there are no more results + * * highlights: a list of words to highlight from the stemming algorithm + * * results: a list of results + * + * Each entry in the results list is a SearchResult. + * + * @returns Promise which resolves: result object + * @returns Rejects: with an error response. */ - - searchRoomEvents(opts) { // TODO: support search groups + const body = { search_categories: { room_events: { @@ -5185,69 +4768,64 @@ body: body }).then(res => this.processRoomEventsSearch(searchResults, res)); } + /** * Take a result from an earlier searchRoomEvents call, and backfill results. * - * @param {object} searchResults the results object to be updated - * @return {Promise} Resolves: updated result object - * @return {Error} Rejects: with an error response. + * @param searchResults - the results object to be updated + * @returns Promise which resolves: updated result object + * @returns Rejects: with an error response. */ - - backPaginateRoomEventsSearch(searchResults) { // TODO: we should implement a backoff (as per scrollback()) to deal more // nicely with HTTP errors. + if (!searchResults.next_batch) { return Promise.reject(new Error("Cannot backpaginate event search any further")); } - if (searchResults.pendingRequest) { // already a request in progress - return the existing promise return searchResults.pendingRequest; } - const searchOpts = { body: searchResults._query, next_batch: searchResults.next_batch }; - const promise = this.search(searchOpts).then(res => this.processRoomEventsSearch(searchResults, res)).finally(() => { - searchResults.pendingRequest = null; + const promise = this.search(searchOpts, searchResults.abortSignal).then(res => this.processRoomEventsSearch(searchResults, res)).finally(() => { + searchResults.pendingRequest = undefined; }); searchResults.pendingRequest = promise; return promise; } + /** * helper for searchRoomEvents and backPaginateRoomEventsSearch. Processes the * response from the API call and updates the searchResults * - * @param {Object} searchResults - * @param {Object} response - * @return {Object} searchResults - * @private + * @returns searchResults + * @internal */ // XXX: Intended private, used in code - - processRoomEventsSearch(searchResults, response) { const roomEvents = response.search_categories.room_events; searchResults.count = roomEvents.count; - searchResults.next_batch = roomEvents.next_batch; // combine the highlight list with our existing list; + searchResults.next_batch = roomEvents.next_batch; + // combine the highlight list with our existing list; const highlights = new Set(roomEvents.highlights); searchResults.highlights.forEach(hl => { highlights.add(hl); - }); // turn it back into a list. + }); + // turn it back into a list. searchResults.highlights = Array.from(highlights); - const mapper = this.getEventMapper(); // append the new results to our existing results + const mapper = this.getEventMapper(); + // append the new results to our existing results const resultsLength = roomEvents.results?.length ?? 0; - for (let i = 0; i < resultsLength; i++) { const sr = _searchResult.SearchResult.fromJson(roomEvents.results[i], mapper); - const room = this.getRoom(sr.context.getEvent().getRoomId()); - if (room) { // Copy over a known event sender if we can for (const ev of sr.context.getTimeline()) { @@ -5255,20 +4833,17 @@ if (!ev.sender && sender) ev.sender = sender; } } - searchResults.results.push(sr); } - return searchResults; } + /** * Populate the store with rooms the user has left. - * @return {Promise} Resolves: TODO - Resolved when the rooms have + * @returns Promise which resolves: TODO - Resolved when the rooms have * been added to the data store. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Rejects: with an error response. */ - - syncLeftRooms() { // Guard against multiple calls whilst ongoing and multiple calls post success if (this.syncedLeftRooms) { @@ -5279,90 +4854,79 @@ return this.syncLeftRoomsPromise; // return the ongoing request } - const syncApi = new _sync.SyncApi(this, this.clientOpts); - this.syncLeftRoomsPromise = syncApi.syncLeftRooms(); // cleanup locks + const syncApi = new _sync.SyncApi(this, this.clientOpts, this.buildSyncApiOptions()); + this.syncLeftRoomsPromise = syncApi.syncLeftRooms(); + // cleanup locks this.syncLeftRoomsPromise.then(() => { _logger.logger.log("Marking success of sync left room request"); - this.syncedLeftRooms = true; // flip the bit on success }).finally(() => { - this.syncLeftRoomsPromise = null; // cleanup ongoing request state + this.syncLeftRoomsPromise = undefined; // cleanup ongoing request state }); + return this.syncLeftRoomsPromise; } + /** * Create a new filter. - * @param {Object} content The HTTP body for the request - * @return {Filter} Resolves to a Filter object. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param content - The HTTP body for the request + * @returns Promise which resolves to a Filter object. + * @returns Rejects: with an error response. */ - - createFilter(content) { const path = utils.encodeUri("/user/$userId/filter", { $userId: this.credentials.userId }); - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, undefined, content).then(response => { + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, content).then(response => { // persist the filter const filter = _filter.Filter.fromJson(this.credentials.userId, response.filter_id, content); - this.store.storeFilter(filter); return filter; }); } + /** * Retrieve a filter. - * @param {string} userId The user ID of the filter owner - * @param {string} filterId The filter ID to retrieve - * @param {boolean} allowCached True to allow cached filters to be returned. + * @param userId - The user ID of the filter owner + * @param filterId - The filter ID to retrieve + * @param allowCached - True to allow cached filters to be returned. * Default: True. - * @return {Promise} Resolves: a Filter object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: a Filter object + * @returns Rejects: with an error response. */ - - getFilter(userId, filterId, allowCached) { if (allowCached) { const filter = this.store.getFilter(userId, filterId); - if (filter) { return Promise.resolve(filter); } } - const path = utils.encodeUri("/user/$userId/filter/$filterId", { $userId: userId, $filterId: filterId }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path).then(response => { + return this.http.authedRequest(_httpApi.Method.Get, path).then(response => { // persist the filter const filter = _filter.Filter.fromJson(userId, filterId, response); - this.store.storeFilter(filter); return filter; }); } + /** - * @param {string} filterName - * @param {Filter} filter - * @return {Promise} Filter ID + * @returns Filter ID */ - - async getOrCreateFilter(filterName, filter) { const filterId = this.store.getFilterIdByName(filterName); - let existingId = undefined; - + let existingId; if (filterId) { // check that the existing filter matches our expectations try { const existingFilter = await this.getFilter(this.credentials.userId, filterId, true); - if (existingFilter) { const oldDef = existingFilter.getDefinition(); const newDef = filter.getDefinition(); - if (utils.deepCompare(oldDef, newDef)) { // super, just use that. // debuglog("Using existing filter ID %s: %s", filterId, @@ -5380,116 +4944,98 @@ if (error.errcode !== "M_UNKNOWN" && error.errcode !== "M_NOT_FOUND") { throw error; } - } // if the filter doesn't exist anymore on the server, remove from store - - + } + // if the filter doesn't exist anymore on the server, remove from store if (!existingId) { this.store.setFilterIdByName(filterName, undefined); } } - if (existingId) { return existingId; - } // create a new filter - - - const createdFilter = await this.createFilter(filter.getDefinition()); // debuglog("Created new filter ID %s: %s", createdFilter.filterId, - // JSON.stringify(createdFilter.getDefinition())); + } + // create a new filter + const createdFilter = await this.createFilter(filter.getDefinition()); this.store.setFilterIdByName(filterName, createdFilter.filterId); return createdFilter.filterId; } + /** * Gets a bearer token from the homeserver that the user can * present to a third party in order to prove their ownership * of the Matrix account they are logged into. - * @return {Promise} Resolves: Token object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Token object + * @returns Rejects: with an error response. */ - - getOpenIdToken() { const path = utils.encodeUri("/user/$userId/openid/request_token", { $userId: this.credentials.userId }); - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, undefined, {}); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, {}); } - /** - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: ITurnServerResponse object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: ITurnServerResponse object + * @returns Rejects: with an error response. */ - turnServer(callback) { - return this.http.authedRequest(callback, _httpApi.Method.Get, "/voip/turnServer"); + turnServer() { + return this.http.authedRequest(_httpApi.Method.Get, "/voip/turnServer"); } + /** * Get the TURN servers for this homeserver. - * @return {Array} The servers or an empty list. + * @returns The servers or an empty list. */ - - getTurnServers() { return this.turnServers || []; } + /** * Get the unix timestamp (in milliseconds) at which the current * TURN credentials (from getTurnServers) expire - * @return {number} The expiry timestamp, in milliseconds, or null if no credentials + * @returns The expiry timestamp in milliseconds */ - - getTurnServersExpiry() { return this.turnServersExpiry; } - get pollingTurnServers() { - return this.checkTurnServersIntervalID !== null; - } // XXX: Intended private, used in code. - + return this.checkTurnServersIntervalID !== undefined; + } + // XXX: Intended private, used in code. async checkTurnServers() { if (!this.canSupportVoip) { return; } - let credentialsGood = false; const remainingTime = this.turnServersExpiry - Date.now(); - if (remainingTime > TURN_CHECK_INTERVAL) { _logger.logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones."); - credentialsGood = true; } else { _logger.logger.debug("Fetching new TURN credentials"); - try { const res = await this.turnServer(); - if (res.uris) { - _logger.logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs"); // map the response to a format that can be fed to RTCPeerConnection - - + _logger.logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs"); + // map the response to a format that can be fed to RTCPeerConnection const servers = { urls: res.uris, username: res.username, credential: res.password }; - this.turnServers = [servers]; // The TTL is in seconds but we work in ms - + this.turnServers = [servers]; + // The TTL is in seconds but we work in ms this.turnServersExpiry = Date.now() + res.ttl * 1000; credentialsGood = true; this.emit(ClientEvent.TurnServers, this.turnServers); } } catch (err) { _logger.logger.error("Failed to get TURN URIs", err); - if (err.httpStatus === 403) { // We got a 403, so there's no point in looping forever. _logger.logger.info("TURN access unavailable for this account: stopping credentials checks"); - if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID); - this.checkTurnServersIntervalID = null; + this.checkTurnServersIntervalID = undefined; this.emit(ClientEvent.TurnServersError, err, true); // fatal } else { // otherwise, if we failed for whatever reason, try again the next time we're called. @@ -5500,105 +5046,98 @@ return credentialsGood; } + /** * Set whether to allow a fallback ICE server should be used for negotiating a * WebRTC connection if the homeserver doesn't provide any servers. Defaults to * false. * - * @param {boolean} allow */ - - setFallbackICEServerAllowed(allow) { this.fallbackICEServerAllowed = allow; } + /** * Get whether to allow a fallback ICE server should be used for negotiating a * WebRTC connection if the homeserver doesn't provide any servers. Defaults to * false. * - * @returns {boolean} + * @returns */ - - isFallbackICEServerAllowed() { return this.fallbackICEServerAllowed; } + /** * Determines if the current user is an administrator of the Synapse homeserver. * Returns false if untrue or the homeserver does not appear to be a Synapse * homeserver. This function is implementation specific and may change * as a result. - * @return {boolean} true if the user appears to be a Synapse administrator. + * @returns true if the user appears to be a Synapse administrator. */ - - isSynapseAdministrator() { const path = utils.encodeUri("/_synapse/admin/v1/users/$userId/admin", { $userId: this.getUserId() }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, undefined, undefined, { - prefix: '' - }).then(r => r['admin']); // pull out the specific boolean we want + return this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, { + prefix: "" + }).then(r => r.admin); // pull out the specific boolean we want } + /** * Performs a whois lookup on a user using Synapse's administrator API. * This function is implementation specific and may change as a * result. - * @param {string} userId the User ID to look up. - * @return {object} the whois response - see Synapse docs for information. + * @param userId - the User ID to look up. + * @returns the whois response - see Synapse docs for information. */ - - whoisSynapseUser(userId) { const path = utils.encodeUri("/_synapse/admin/v1/whois/$userId", { $userId: userId }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, undefined, undefined, { - prefix: '' + return this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, { + prefix: "" }); } + /** * Deactivates a user using Synapse's administrator API. This * function is implementation specific and may change as a result. - * @param {string} userId the User ID to deactivate. - * @return {object} the deactivate response - see Synapse docs for information. + * @param userId - the User ID to deactivate. + * @returns the deactivate response - see Synapse docs for information. */ - - deactivateSynapseUser(userId) { const path = utils.encodeUri("/_synapse/admin/v1/deactivate/$userId", { $userId: userId }); - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, undefined, undefined, { - prefix: '' + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, undefined, { + prefix: "" }); } - async fetchClientWellKnown() { // `getRawClientConfig` does not throw or reject on network errors, instead // it absorbs errors and returns `{}`. - this.clientWellKnownPromise = _autodiscovery.AutoDiscovery.getRawClientConfig(this.getDomain()); + this.clientWellKnownPromise = _autodiscovery.AutoDiscovery.getRawClientConfig(this.getDomain() ?? undefined); this.clientWellKnown = await this.clientWellKnownPromise; this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown); } - getClientWellKnown() { return this.clientWellKnown; } - waitForClientWellKnown() { + if (!this.clientRunning) { + throw new Error("Client is not running"); + } return this.clientWellKnownPromise; } + /** * store client options with boolean/string/numeric values * to know in the next session what flags the sync data was * created with (e.g. lazy loading) - * @param {object} opts the complete set of client options - * @return {Promise} for store operation + * @param opts - the complete set of client options + * @returns for store operation */ - - storeClientOptions() { // XXX: Intended private, used in code const primTypes = ["boolean", "string", "number"]; @@ -5610,75 +5149,71 @@ }, {}); return this.store.storeClientOptions(serializableOpts); } + /** * Gets a set of room IDs in common with another user - * @param {string} userId The userId to check. - * @return {Promise} Resolves to a set of rooms - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param userId - The userId to check. + * @returns Promise which resolves to a set of rooms + * @returns Rejects: with an error response. */ - - + // eslint-disable-next-line async _unstable_getSharedRooms(userId) { - // eslint-disable-line const sharedRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"); const mutualRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666.mutual_rooms"); - if (!sharedRoomsSupport && !mutualRoomsSupport) { - throw Error('Server does not support mutual_rooms API'); + throw Error("Server does not support mutual_rooms API"); } - - const path = utils.encodeUri(`/uk.half-shot.msc2666/user/${mutualRoomsSupport ? 'mutual_rooms' : 'shared_rooms'}/$userId`, { + const path = utils.encodeUri(`/uk.half-shot.msc2666/user/${mutualRoomsSupport ? "mutual_rooms" : "shared_rooms"}/$userId`, { $userId: userId }); - const res = await this.http.authedRequest(undefined, _httpApi.Method.Get, path, undefined, undefined, { - prefix: _httpApi.PREFIX_UNSTABLE + const res = await this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, { + prefix: _httpApi.ClientPrefix.Unstable }); return res.joined; } + /** * Get the API versions supported by the server, along with any * unstable APIs it supports - * @return {Promise} The server /versions response + * @returns The server /versions response */ - - - getVersions() { + async getVersions() { if (this.serverVersionsPromise) { return this.serverVersionsPromise; } - - this.serverVersionsPromise = this.http.request(undefined, // callback - _httpApi.Method.Get, "/_matrix/client/versions", undefined, // queryParams - undefined, // data + this.serverVersionsPromise = this.http.request(_httpApi.Method.Get, "/_matrix/client/versions", undefined, + // queryParams + undefined, + // data { - prefix: '' + prefix: "" }).catch(e => { // Need to unset this if it fails, otherwise we'll never retry - this.serverVersionsPromise = null; // but rethrow the exception to anything that was waiting - + this.serverVersionsPromise = undefined; + // but rethrow the exception to anything that was waiting throw e; }); + const serverVersions = await this.serverVersionsPromise; + this.canSupport = await (0, _feature.buildFeatureSupportMap)(serverVersions); return this.serverVersionsPromise; } + /** * Check if a particular spec version is supported by the server. - * @param {string} version The spec version (such as "r0.5.0") to check for. - * @return {Promise} Whether it is supported + * @param version - The spec version (such as "r0.5.0") to check for. + * @returns Whether it is supported */ - - async isVersionSupported(version) { const { versions } = await this.getVersions(); return versions && versions.includes(version); } + /** * Query the server to see if it supports members lazy loading - * @return {Promise} true if server supports lazy loading + * @returns true if server supports lazy loading */ - - async doesServerSupportLazyLoading() { const response = await this.getVersions(); if (!response) return false; @@ -5686,39 +5221,36 @@ const unstableFeatures = response["unstable_features"]; return versions && versions.includes("r0.5.0") || unstableFeatures && unstableFeatures["m.lazy_load_members"]; } + /** * Query the server to see if the `id_server` parameter is required * when registering with an 3pid, adding a 3pid or resetting password. - * @return {Promise} true if id_server parameter is required + * @returns true if id_server parameter is required */ - - async doesServerRequireIdServerParam() { const response = await this.getVersions(); if (!response) return true; - const versions = response["versions"]; // Supporting r0.6.0 is the same as having the flag set to false + const versions = response["versions"]; + // Supporting r0.6.0 is the same as having the flag set to false if (versions && versions.includes("r0.6.0")) { return false; } - const unstableFeatures = response["unstable_features"]; if (!unstableFeatures) return true; - if (unstableFeatures["m.require_identity_server"] === undefined) { return true; } else { return unstableFeatures["m.require_identity_server"]; } } + /** * Query the server to see if the `id_access_token` parameter can be safely * passed to the homeserver. Some homeservers may trigger errors if they are not * prepared for the new parameter. - * @return {Promise} true if id_access_token can be sent + * @returns true if id_access_token can be sent */ - - async doesServerAcceptIdentityAccessToken() { const response = await this.getVersions(); if (!response) return false; @@ -5726,14 +5258,13 @@ const unstableFeatures = response["unstable_features"]; return versions && versions.includes("r0.6.0") || unstableFeatures && unstableFeatures["m.id_access_token"]; } + /** * Query the server to see if it supports separate 3PID add and bind functions. * This affects the sequence of API calls clients should use for these operations, * so it's helpful to be able to check for support. - * @return {Promise} true if separate functions are supported + * @returns true if separate functions are supported */ - - async doesServerSupportSeparateAddAndBind() { const response = await this.getVersions(); if (!response) return false; @@ -5741,71 +5272,76 @@ const unstableFeatures = response["unstable_features"]; return versions?.includes("r0.6.0") || unstableFeatures?.["m.separate_add_and_bind"]; } + /** * Query the server to see if it lists support for an unstable feature * in the /versions response - * @param {string} feature the feature name - * @return {Promise} true if the feature is supported + * @param feature - the feature name + * @returns true if the feature is supported */ - - async doesServerSupportUnstableFeature(feature) { const response = await this.getVersions(); if (!response) return false; const unstableFeatures = response["unstable_features"]; return unstableFeatures && !!unstableFeatures[feature]; } + /** * Query the server to see if it is forcing encryption to be enabled for * a given room preset, based on the /versions response. - * @param {Preset} presetName The name of the preset to check. - * @returns {Promise} true if the server is forcing encryption + * @param presetName - The name of the preset to check. + * @returns true if the server is forcing encryption * for the preset. */ - - async doesServerForceEncryptionForPreset(presetName) { const response = await this.getVersions(); if (!response) return false; - const unstableFeatures = response["unstable_features"]; // The preset name in the versions response will be without the _chat suffix. + const unstableFeatures = response["unstable_features"]; + // The preset name in the versions response will be without the _chat suffix. const versionsPresetName = presetName.includes("_chat") ? presetName.substring(0, presetName.indexOf("_chat")) : presetName; return unstableFeatures && !!unstableFeatures[`io.element.e2ee_forced.${versionsPresetName}`]; } - async doesServerSupportThread() { + if (await this.isVersionSupported("v1.4")) { + return { + threads: _thread.FeatureSupport.Stable, + list: _thread.FeatureSupport.Stable, + fwdPagination: _thread.FeatureSupport.Stable + }; + } try { - const hasUnstableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440"); - const hasStableSupport = await this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"); // TODO: Use `this.isVersionSupported("v1.3")` for whatever spec version includes MSC3440 formally. - + const [threadUnstable, threadStable, listUnstable, listStable, fwdPaginationUnstable, fwdPaginationStable] = await Promise.all([this.doesServerSupportUnstableFeature("org.matrix.msc3440"), this.doesServerSupportUnstableFeature("org.matrix.msc3440.stable"), this.doesServerSupportUnstableFeature("org.matrix.msc3856"), this.doesServerSupportUnstableFeature("org.matrix.msc3856.stable"), this.doesServerSupportUnstableFeature("org.matrix.msc3715"), this.doesServerSupportUnstableFeature("org.matrix.msc3715.stable")]); return { - serverSupport: hasUnstableSupport || hasStableSupport, - stable: hasStableSupport + threads: (0, _thread.determineFeatureSupport)(threadStable, threadUnstable), + list: (0, _thread.determineFeatureSupport)(listStable, listUnstable), + fwdPagination: (0, _thread.determineFeatureSupport)(fwdPaginationStable, fwdPaginationUnstable) }; } catch (e) { - // Assume server support and stability aren't available: null/no data return. - // XXX: This should just return an object with `false` booleans instead. - return null; + return { + threads: _thread.FeatureSupport.None, + list: _thread.FeatureSupport.None, + fwdPagination: _thread.FeatureSupport.None + }; } } + /** * Query the server to see if it supports the MSC2457 `logout_devices` parameter when setting password - * @return {Promise} true if server supports the `logout_devices` parameter + * @returns true if server supports the `logout_devices` parameter */ - - doesServerSupportLogoutDevices() { return this.isVersionSupported("r0.6.1"); } + /** * Get if lazy loading members is being used. - * @return {boolean} Whether or not members are lazy loaded by this client + * @returns Whether or not members are lazy loaded by this client */ - - hasLazyLoadMembersEnabled() { - return !!this.clientOpts.lazyLoadMembers; + return !!this.clientOpts?.lazyLoadMembers; } + /** * Set a function which is called when /sync returns a 'limited' response. * It is called with a room ID and returns a boolean. It should return 'true' if the SDK @@ -5813,198 +5349,170 @@ * are other references to the timelines for this room, e.g because the client is * actively viewing events in this room. * Default: returns false. - * @param {Function} cb The callback which will be invoked. + * @param cb - The callback which will be invoked. */ - - setCanResetTimelineCallback(cb) { this.canResetTimelineCallback = cb; } + /** * Get the callback set via `setCanResetTimelineCallback`. - * @return {?Function} The callback or null + * @returns The callback or null */ - - getCanResetTimelineCallback() { return this.canResetTimelineCallback; } + /** * Returns relations for a given event. Handles encryption transparently, * with the caveat that the amount of events returned might be 0, even though you get a nextBatch. * When the returned promise resolves, all messages should have finished trying to decrypt. - * @param {string} roomId the room of the event - * @param {string} eventId the id of the event - * @param {string} relationType the rel_type of the relations requested - * @param {string} eventType the event type of the relations requested - * @param {Object} opts options with optional values for the request. - * @return {Object} an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available. + * @param roomId - the room of the event + * @param eventId - the id of the event + * @param relationType - the rel_type of the relations requested + * @param eventType - the event type of the relations requested + * @param opts - options with optional values for the request. + * @returns an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available. */ - - async relations(roomId, eventId, relationType, eventType, opts = { - direction: _eventTimeline.Direction.Backward + dir: _eventTimeline.Direction.Backward }) { - const fetchedEventType = this.getEncryptedIfNeededEventType(roomId, eventType); - const result = await this.fetchRelations(roomId, eventId, relationType, fetchedEventType, opts); + const fetchedEventType = eventType ? this.getEncryptedIfNeededEventType(roomId, eventType) : null; + const [eventResult, result] = await Promise.all([this.fetchRoomEvent(roomId, eventId), this.fetchRelations(roomId, eventId, relationType, fetchedEventType, opts)]); const mapper = this.getEventMapper(); - const originalEvent = result.original_event ? mapper(result.original_event) : undefined; + const originalEvent = eventResult ? mapper(eventResult) : undefined; let events = result.chunk.map(mapper); - if (fetchedEventType === _event2.EventType.RoomMessageEncrypted) { const allEvents = originalEvent ? events.concat(originalEvent) : events; await Promise.all(allEvents.map(e => this.decryptEventIfNeeded(e))); - if (eventType !== null) { events = events.filter(e => e.getType() === eventType); } } - if (originalEvent && relationType === _event2.RelationType.Replace) { events = events.filter(e => e.getSender() === originalEvent.getSender()); } - return { - originalEvent, + originalEvent: originalEvent ?? null, events, - nextBatch: result.next_batch, - prevBatch: result.prev_batch + nextBatch: result.next_batch ?? null, + prevBatch: result.prev_batch ?? null }; } + /** * The app may wish to see if we have a key cached without * triggering a user interaction. - * @return {object} */ - - getCrossSigningCacheCallbacks() { // XXX: Private member access return this.crypto?.crossSigningInfo.getCacheCallbacks(); } + /** * Generates a random string suitable for use as a client secret. This * method is experimental and may change. - * @return {string} A new client secret + * @returns A new client secret */ - - generateClientSecret() { return (0, _randomstring.randomString)(32); } + /** * Attempts to decrypt an event - * @param {MatrixEvent} event The event to decrypt - * @returns {Promise} A decryption promise - * @param {object} options - * @param {boolean} options.isRetry True if this is a retry (enables more logging) - * @param {boolean} options.emit Emits "event.decrypted" if set to true + * @param event - The event to decrypt + * @returns A decryption promise */ - - decryptEventIfNeeded(event, options) { - if (event.shouldAttemptDecryption()) { - event.attemptDecryption(this.crypto, options); + if (event.shouldAttemptDecryption() && this.isCryptoEnabled()) { + event.attemptDecryption(this.cryptoBackend, options); } - if (event.isBeingDecrypted()) { return event.getDecryptionPromise(); } else { return Promise.resolve(); } } - termsUrlForService(serviceType, baseUrl) { switch (serviceType) { case _serviceTypes.SERVICE_TYPES.IS: - return baseUrl + _httpApi.PREFIX_IDENTITY_V2 + '/terms'; - + return this.http.getUrl("/terms", undefined, _httpApi.IdentityPrefix.V2, baseUrl); case _serviceTypes.SERVICE_TYPES.IM: - return baseUrl + '/_matrix/integrations/v1/terms'; - + return this.http.getUrl("/terms", undefined, "/_matrix/integrations/v1", baseUrl); default: - throw new Error('Unsupported service type'); + throw new Error("Unsupported service type"); } } + /** * Get the Homeserver URL of this client - * @return {string} Homeserver URL of this client + * @returns Homeserver URL of this client */ - - getHomeserverUrl() { return this.baseUrl; } + /** * Get the identity server URL of this client - * @param {boolean} stripProto whether or not to strip the protocol from the URL - * @return {string} Identity server URL of this client + * @param stripProto - whether or not to strip the protocol from the URL + * @returns Identity server URL of this client */ - - getIdentityServerUrl(stripProto = false) { - if (stripProto && (this.idBaseUrl.startsWith("http://") || this.idBaseUrl.startsWith("https://"))) { + if (stripProto && (this.idBaseUrl?.startsWith("http://") || this.idBaseUrl?.startsWith("https://"))) { return this.idBaseUrl.split("://")[1]; } - return this.idBaseUrl; } + /** * Set the identity server URL of this client - * @param {string} url New identity server URL + * @param url - New identity server URL */ - - setIdentityServerUrl(url) { this.idBaseUrl = utils.ensureNoTrailingSlash(url); this.http.setIdBaseUrl(this.idBaseUrl); } + /** * Get the access token associated with this account. - * @return {?String} The access_token or null + * @returns The access_token or null */ - - getAccessToken() { return this.http.opts.accessToken || null; } + /** * Set the access token associated with this account. - * @param {string} token The new access token. + * @param token - The new access token. */ - - setAccessToken(token) { this.http.opts.accessToken = token; } + /** - * @return {boolean} true if there is a valid access_token for this client. + * @returns true if there is a valid access_token for this client. */ - - isLoggedIn() { return this.http.opts.accessToken !== undefined; } + /** * Make up a new transaction id * - * @return {string} a new, unique, transaction id + * @returns a new, unique, transaction id */ - - makeTxnId() { return "m" + new Date().getTime() + "." + this.txnCtr++; } + /** * Check whether a username is available prior to registration. An error response * indicates an invalid/unavailable username. - * @param {string} username The username to check the availability of. - * @return {Promise} Resolves: to boolean of whether the username is available. + * @param username - The username to check the availability of. + * @returns Promise which resolves: to boolean of whether the username is available. */ - - isUsernameAvailable(username) { - return this.http.authedRequest(undefined, _httpApi.Method.Get, '/register/available', { + return this.http.authedRequest(_httpApi.Method.Get, "/register/available", { username }).then(response => { return response.available; @@ -6012,27 +5520,18 @@ if (response.errcode === "M_USER_IN_USE") { return false; } - return Promise.reject(response); }); } + /** - * @param {string} username - * @param {string} password - * @param {string} sessionId - * @param {Object} auth - * @param {Object} bindThreepids Set key 'email' to true to bind any email + * @param bindThreepids - Set key 'email' to true to bind any email * threepid uses during registration in the identity server. Set 'msisdn' to * true to bind msisdn. - * @param {string} guestAccessToken - * @param {string} inhibitLogin - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - register(username, password, sessionId, auth, bindThreepids, guestAccessToken, inhibitLogin, callback) { + register(username, password, sessionId, auth, bindThreepids, guestAccessToken, inhibitLogin) { // backwards compat if (bindThreepids === true) { bindThreepids = { @@ -6041,58 +5540,44 @@ } else if (bindThreepids === null || bindThreepids === undefined || bindThreepids === false) { bindThreepids = {}; } - - if (typeof inhibitLogin === 'function') { - callback = inhibitLogin; - inhibitLogin = undefined; - } - if (sessionId) { auth.session = sessionId; } - const params = { auth: auth, refresh_token: true // always ask for a refresh token - does nothing if unsupported - }; if (username !== undefined && username !== null) { params.username = username; } - if (password !== undefined && password !== null) { params.password = password; } - if (bindThreepids.email) { params.bind_email = true; } - if (bindThreepids.msisdn) { params.bind_msisdn = true; } - if (guestAccessToken !== undefined && guestAccessToken !== null) { params.guest_access_token = guestAccessToken; } - if (inhibitLogin !== undefined && inhibitLogin !== null) { params.inhibit_login = inhibitLogin; - } // Temporary parameter added to make the register endpoint advertise + } + // Temporary parameter added to make the register endpoint advertise // msisdn flows. This exists because there are clients that break // when given stages they don't recognise. This parameter will cease // to be necessary once these old clients are gone. // Only send it if we send any params at all (the password param is // mandatory, so if we send any params, we'll send the password param) - - if (password !== undefined && password !== null) { params.x_show_msisdn = true; } - - return this.registerRequest(params, undefined, callback); + return this.registerRequest(params); } + /** * Register a guest account. * This method returns the auth info needed to create a new authenticated client, @@ -6109,39 +5594,32 @@ * client.setGuest(true); * ``` * - * @param {Object=} opts Registration options - * @param {Object} opts.body JSON HTTP body to provide. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: JSON object that contains: - * { user_id, device_id, access_token, home_server } - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ - - - registerGuest(opts, callback) { + * @param body - JSON HTTP body to provide. + * @returns Promise which resolves: JSON object that contains: + * `{ user_id, device_id, access_token, home_server }` + * @returns Rejects: with an error response. + */ + registerGuest({ + body + } = {}) { // TODO: Types - opts = opts || {}; - opts.body = opts.body || {}; - return this.registerRequest(opts.body, "guest", callback); + return this.registerRequest(body || {}, "guest"); } + /** - * @param {Object} data parameters for registration request - * @param {string=} kind type of user to register. may be "guest" - * @param {module:client.callback=} callback - * @return {Promise} Resolves: to the /register response - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param data - parameters for registration request + * @param kind - type of user to register. may be "guest" + * @returns Promise which resolves: to the /register response + * @returns Rejects: with an error response. */ - - - registerRequest(data, kind, callback) { + registerRequest(data, kind) { const params = {}; - if (kind) { params.kind = kind; } - - return this.http.request(callback, _httpApi.Method.Post, "/register", params, data); + return this.http.request(_httpApi.Method.Post, "/register", params, data); } + /** * Refreshes an access token using a provided refresh token. The refresh token * must be valid for the current access token known to the client instance. @@ -6149,152 +5627,126 @@ * Note that this function will not cause a logout if the token is deemed * unknown by the server - the caller is responsible for managing logout * actions on error. - * @param {string} refreshToken The refresh token. - * @return {Promise} Resolves to the new token. - * @return {module:http-api.MatrixError} Rejects with an error response. + * @param refreshToken - The refresh token. + * @returns Promise which resolves to the new token. + * @returns Rejects with an error response. */ - - refreshToken(refreshToken) { - return this.http.authedRequest(undefined, _httpApi.Method.Post, "/refresh", undefined, { + return this.http.authedRequest(_httpApi.Method.Post, "/refresh", undefined, { refresh_token: refreshToken }, { - prefix: _httpApi.PREFIX_V1, + prefix: _httpApi.ClientPrefix.V1, inhibitLogoutEmit: true // we don't want to cause logout loops - }); } + /** - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves to the available login flows + * @returns Rejects: with an error response. */ - - - loginFlows(callback) { - // TODO: Types - return this.http.request(callback, _httpApi.Method.Get, "/login"); + loginFlows() { + return this.http.request(_httpApi.Method.Get, "/login"); } + /** - * @param {string} loginType - * @param {Object} data - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - login(loginType, data, callback) { + login(loginType, data) { // TODO: Types const loginData = { type: loginType - }; // merge data into loginData + }; + // merge data into loginData Object.assign(loginData, data); - return this.http.authedRequest((error, response) => { - if (response && response.access_token && response.user_id) { + return this.http.authedRequest(_httpApi.Method.Post, "/login", undefined, loginData).then(response => { + if (response.access_token && response.user_id) { this.http.opts.accessToken = response.access_token; this.credentials = { userId: response.user_id }; } - - if (callback) { - callback(error, response); - } - }, _httpApi.Method.Post, "/login", undefined, loginData); + return response; + }); } + /** - * @param {string} user - * @param {string} password - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - loginWithPassword(user, password, callback) { + loginWithPassword(user, password) { // TODO: Types return this.login("m.login.password", { user: user, password: password - }, callback); + }); } + /** - * @param {string} relayState URL Callback after SAML2 Authentication - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param relayState - URL Callback after SAML2 Authentication + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - loginWithSAML2(relayState, callback) { + loginWithSAML2(relayState) { // TODO: Types return this.login("m.login.saml2", { relay_state: relayState - }, callback); + }); } + /** - * @param {string} redirectUrl The URL to redirect to after the HS + * @param redirectUrl - The URL to redirect to after the HS * authenticates with CAS. - * @return {string} The HS URL to hit to begin the CAS login process. + * @returns The HS URL to hit to begin the CAS login process. */ - - getCasLoginUrl(redirectUrl) { return this.getSsoLoginUrl(redirectUrl, "cas"); } + /** - * @param {string} redirectUrl The URL to redirect to after the HS + * @param redirectUrl - The URL to redirect to after the HS * authenticates with the SSO. - * @param {string} loginType The type of SSO login we are doing (sso or cas). + * @param loginType - The type of SSO login we are doing (sso or cas). * Defaults to 'sso'. - * @param {string} idpId The ID of the Identity Provider being targeted, optional. - * @param {SSOAction} action the SSO flow to indicate to the IdP, optional. - * @return {string} The HS URL to hit to begin the SSO login process. + * @param idpId - The ID of the Identity Provider being targeted, optional. + * @param action - the SSO flow to indicate to the IdP, optional. + * @returns The HS URL to hit to begin the SSO login process. */ - - getSsoLoginUrl(redirectUrl, loginType = "sso", idpId, action) { let url = "/login/" + loginType + "/redirect"; - if (idpId) { url += "/" + idpId; } - const params = { redirectUrl, [SSO_ACTION_PARAM.unstable]: action }; - return this.http.getUrl(url, params, _httpApi.PREFIX_R0); + return this.http.getUrl(url, params, _httpApi.ClientPrefix.R0).href; } + /** - * @param {string} token Login token previously received from homeserver - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param token - Login token previously received from homeserver + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - loginWithToken(token, callback) { + loginWithToken(token) { // TODO: Types return this.login("m.login.token", { token: token - }, callback); + }); } + /** * Logs out the current session. * Obviously, further calls that require authorisation should fail after this * method is called. The state of the MatrixClient object is not affected: * it is up to the caller to either reset or destroy the MatrixClient after * this method succeeds. - * @param {module:client.callback} callback Optional. - * @param {boolean} stopClient whether to stop the client before calling /logout to prevent invalid token errors. - * @return {Promise} Resolves: On success, the empty object {} + * @param stopClient - whether to stop the client before calling /logout to prevent invalid token errors. + * @returns Promise which resolves: On success, the empty object `{}` */ - - - async logout(callback, stopClient = false) { + async logout(stopClient = false) { if (this.crypto?.backupManager?.getKeyBackupEnabled()) { try { while ((await this.crypto.backupManager.backupPendingKeys(200)) > 0); @@ -6302,306 +5754,264 @@ _logger.logger.error("Key backup request failed when logging out. Some keys may be missing from backup", err); } } - if (stopClient) { this.stopClient(); + this.http.abort(); } - - return this.http.authedRequest(callback, _httpApi.Method.Post, '/logout'); + return this.http.authedRequest(_httpApi.Method.Post, "/logout"); } + /** * Deactivates the logged-in account. * Obviously, further calls that require authorisation should fail after this * method is called. The state of the MatrixClient object is not affected: * it is up to the caller to either reset or destroy the MatrixClient after * this method succeeds. - * @param {object} auth Optional. Auth data to supply for User-Interactive auth. - * @param {boolean} erase Optional. If set, send as `erase` attribute in the + * @param auth - Optional. Auth data to supply for User-Interactive auth. + * @param erase - Optional. If set, send as `erase` attribute in the * JSON request body, indicating whether the account should be erased. Defaults * to false. - * @return {Promise} Resolves: On success, the empty object + * @returns Promise which resolves: On success, the empty object */ - - deactivateAccount(auth, erase) { - if (typeof erase === 'function') { - throw new Error('deactivateAccount no longer accepts a callback parameter'); - } - const body = {}; - if (auth) { body.auth = auth; } - if (erase !== undefined) { body.erase = erase; } + return this.http.authedRequest(_httpApi.Method.Post, "/account/deactivate", undefined, body); + } - return this.http.authedRequest(undefined, _httpApi.Method.Post, '/account/deactivate', undefined, body); + /** + * Make a request for an `m.login.token` to be issued as per + * [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882). + * The server may require User-Interactive auth. + * Note that this is UNSTABLE and subject to breaking changes without notice. + * @param auth - Optional. Auth data to supply for User-Interactive auth. + * @returns Promise which resolves: On success, the token response + * or UIA auth data. + */ + requestLoginToken(auth) { + const body = { + auth + }; + return this.http.authedRequest(_httpApi.Method.Post, "/org.matrix.msc3882/login/token", undefined, + // no query params + body, { + prefix: _httpApi.ClientPrefix.Unstable + }); } + /** * Get the fallback URL to use for unknown interactive-auth stages. * - * @param {string} loginType the type of stage being attempted - * @param {string} authSessionId the auth session ID provided by the homeserver + * @param loginType - the type of stage being attempted + * @param authSessionId - the auth session ID provided by the homeserver * - * @return {string} HS URL to hit to for the fallback interface + * @returns HS URL to hit to for the fallback interface */ - - getFallbackAuthUrl(loginType, authSessionId) { const path = utils.encodeUri("/auth/$loginType/fallback/web", { $loginType: loginType }); return this.http.getUrl(path, { session: authSessionId - }, _httpApi.PREFIX_R0); + }, _httpApi.ClientPrefix.R0).href; } + /** * Create a new room. - * @param {Object} options a list of options to pass to the /createRoom API. - * @param {string} options.room_alias_name The alias localpart to assign to - * this room. - * @param {string} options.visibility Either 'public' or 'private'. - * @param {string[]} options.invite A list of user IDs to invite to this room. - * @param {string} options.name The name to give this room. - * @param {string} options.topic The topic to give this room. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: {room_id: {string}} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param options - a list of options to pass to the /createRoom API. + * @returns Promise which resolves: `{room_id: {string}}` + * @returns Rejects: with an error response. */ - - - async createRoom(options, callback) { + async createRoom(options) { // eslint-disable-line camelcase // some valid options include: room_alias_name, visibility, invite + // inject the id_access_token if inviting 3rd party addresses const invitesNeedingToken = (options.invite_3pid || []).filter(i => !i.id_access_token); - if (invitesNeedingToken.length > 0 && this.identityServer?.getAccessToken && (await this.doesServerAcceptIdentityAccessToken())) { const identityAccessToken = await this.identityServer.getAccessToken(); - if (identityAccessToken) { for (const invite of invitesNeedingToken) { invite.id_access_token = identityAccessToken; } } } - - return this.http.authedRequest(callback, _httpApi.Method.Post, "/createRoom", undefined, options); + return this.http.authedRequest(_httpApi.Method.Post, "/createRoom", undefined, options); } + /** * Fetches relations for a given event - * @param {string} roomId the room of the event - * @param {string} eventId the id of the event - * @param {string} [relationType] the rel_type of the relations requested - * @param {string} [eventType] the event type of the relations requested - * @param {Object} [opts] options with optional values for the request. - * @return {Object} the response, with chunk, prev_batch and, next_batch. + * @param roomId - the room of the event + * @param eventId - the id of the event + * @param relationType - the rel_type of the relations requested + * @param eventType - the event type of the relations requested + * @param opts - options with optional values for the request. + * @returns the response, with chunk, prev_batch and, next_batch. */ - - fetchRelations(roomId, eventId, relationType, eventType, opts = { - direction: _eventTimeline.Direction.Backward + dir: _eventTimeline.Direction.Backward }) { - const queryString = utils.encodeParams(opts); + let params = opts; + if (_thread.Thread.hasServerSideFwdPaginationSupport === _thread.FeatureSupport.Experimental) { + params = (0, utils.replaceParam)("dir", "org.matrix.msc3715.dir", params); + } + const queryString = utils.encodeParams(params); let templatedUrl = "/rooms/$roomId/relations/$eventId"; - if (relationType !== null) { templatedUrl += "/$relationType"; - if (eventType !== null) { templatedUrl += "/$eventType"; } } else if (eventType !== null) { _logger.logger.warn(`eventType: ${eventType} ignored when fetching relations as relationType is null`); - eventType = null; } - const path = utils.encodeUri(templatedUrl + "?" + queryString, { $roomId: roomId, $eventId: eventId, $relationType: relationType, $eventType: eventType }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, null, null, { - prefix: _httpApi.PREFIX_UNSTABLE + return this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, { + prefix: _httpApi.ClientPrefix.V1 }); } + /** - * @param {string} roomId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - roomState(roomId, callback) { + roomState(roomId) { const path = utils.encodeUri("/rooms/$roomId/state", { $roomId: roomId }); - return this.http.authedRequest(callback, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** * Get an event in a room by its event id. - * @param {string} roomId - * @param {string} eventId - * @param {module:client.callback} callback Optional. * - * @return {Promise} Resolves to an object containing the event. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves to an object containing the event. + * @returns Rejects: with an error response. */ - - - fetchRoomEvent(roomId, eventId, callback) { + fetchRoomEvent(roomId, eventId) { const path = utils.encodeUri("/rooms/$roomId/event/$eventId", { $roomId: roomId, $eventId: eventId }); - return this.http.authedRequest(callback, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** - * @param {string} roomId - * @param {string} includeMembership the membership type to include in the response - * @param {string} excludeMembership the membership type to exclude from the response - * @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: dictionary of userid to profile information - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param includeMembership - the membership type to include in the response + * @param excludeMembership - the membership type to exclude from the response + * @param atEventId - the id of the event for which moment in the timeline the members should be returned for + * @returns Promise which resolves: dictionary of userid to profile information + * @returns Rejects: with an error response. */ - - - members(roomId, includeMembership, excludeMembership, atEventId, callback) { + members(roomId, includeMembership, excludeMembership, atEventId) { const queryParams = {}; - if (includeMembership) { queryParams.membership = includeMembership; } - if (excludeMembership) { queryParams.not_membership = excludeMembership; } - if (atEventId) { queryParams.at = atEventId; } - const queryString = utils.encodeParams(queryParams); const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, { $roomId: roomId }); - return this.http.authedRequest(callback, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** * Upgrades a room to a new protocol version - * @param {string} roomId - * @param {string} newVersion The target version to upgrade to - * @return {Promise} Resolves: Object with key 'replacement_room' - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param newVersion - The target version to upgrade to + * @returns Promise which resolves: Object with key 'replacement_room' + * @returns Rejects: with an error response. */ - - upgradeRoom(roomId, newVersion) { // eslint-disable-line camelcase const path = utils.encodeUri("/rooms/$roomId/upgrade", { $roomId: roomId }); - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, undefined, { + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, { new_version: newVersion }); } + /** * Retrieve a state event. - * @param {string} roomId - * @param {string} eventType - * @param {string} stateKey - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - getStateEvent(roomId, eventType, stateKey, callback) { + getStateEvent(roomId, eventType, stateKey) { const pathParams = { $roomId: roomId, $eventType: eventType, $stateKey: stateKey }; let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); - if (stateKey !== undefined) { path = utils.encodeUri(path + "/$stateKey", pathParams); } - - return this.http.authedRequest(callback, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** - * @param {string} roomId - * @param {string} eventType - * @param {Object} content - * @param {string} stateKey - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param opts - Options for the request function. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - sendStateEvent(roomId, eventType, content, stateKey = "", callback) { + sendStateEvent(roomId, eventType, content, stateKey = "", opts = {}) { const pathParams = { $roomId: roomId, $eventType: eventType, $stateKey: stateKey }; let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams); - if (stateKey !== undefined) { path = utils.encodeUri(path + "/$stateKey", pathParams); } - - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, content); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, content, opts); } + /** - * @param {string} roomId - * @param {Number} limit - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - roomInitialSync(roomId, limit, callback) { - if (utils.isFunction(limit)) { - callback = limit; // legacy - - limit = undefined; - } - + roomInitialSync(roomId, limit) { const path = utils.encodeUri("/rooms/$roomId/initialSync", { $roomId: roomId }); - return this.http.authedRequest(callback, _httpApi.Method.Get, path, { + return this.http.authedRequest(_httpApi.Method.Get, path, { limit: limit?.toString() ?? "30" }); } + /** * Set a marker to indicate the point in a room before which the user has read every * event. This can be retrieved from room account data (the event type is `m.fully_read`) * and displayed as a horizontal line in the timeline that is visually distinct to the * position of the user's own read receipt. - * @param {string} roomId ID of the room that has been read - * @param {string} rmEventId ID of the event that has been read - * @param {string} rrEventId ID of the event tracked by the read receipt. This is here + * @param roomId - ID of the room that has been read + * @param rmEventId - ID of the event that has been read + * @param rrEventId - ID of the event tracked by the read receipt. This is here * for convenience because the RR and the RM are commonly updated at the same time as * each other. Optional. - * @param {string} rpEventId rpEvent the m.read.private read receipt event for when we + * @param rpEventId - rpEvent the m.read.private read receipt event for when we * don't want other users to see the read receipts. This is experimental. Optional. - * @return {Promise} Resolves: the empty object, {}. + * @returns Promise which resolves: the empty object, `{}`. */ - - async setRoomReadMarkersHttpRequest(roomId, rmEventId, rrEventId, rpEventId) { const path = utils.encodeUri("/rooms/$roomId/read_markers", { $roomId: roomId @@ -6610,347 +6020,275 @@ [_read_receipts.ReceiptType.FullyRead]: rmEventId, [_read_receipts.ReceiptType.Read]: rrEventId }; - - if (await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) { + if ((await this.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) || (await this.isVersionSupported("v1.4"))) { content[_read_receipts.ReceiptType.ReadPrivate] = rpEventId; } - - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, undefined, content); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, content); } + /** - * @return {Promise} Resolves: A list of the user's current rooms - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: A list of the user's current rooms + * @returns Rejects: with an error response. */ - - getJoinedRooms() { const path = utils.encodeUri("/joined_rooms", {}); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** * Retrieve membership info. for a room. - * @param {string} roomId ID of the room to get membership for - * @return {Promise} Resolves: A list of currently joined users + * @param roomId - ID of the room to get membership for + * @returns Promise which resolves: A list of currently joined users * and their profile data. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Rejects: with an error response. */ - - getJoinedRoomMembers(roomId) { const path = utils.encodeUri("/rooms/$roomId/joined_members", { $roomId: roomId }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** - * @param {Object} options Options for this request - * @param {string} options.server The remote server to query for the room list. + * @param options - Options for this request + * @param server - The remote server to query for the room list. * Optional. If unspecified, get the local home * server's public room list. - * @param {number} options.limit Maximum number of entries to return - * @param {string} options.since Token to paginate from - * @param {object} options.filter Filter parameters - * @param {string} options.filter.generic_search_term String to search for - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ - - - publicRooms(options, callback) { - if (typeof options == 'function') { - callback = options; - options = {}; - } - - if (options === undefined) { - options = {}; - } - - const queryParams = {}; - - if (options.server) { - queryParams.server = options.server; - delete options.server; - } - - if (Object.keys(options).length === 0 && Object.keys(queryParams).length === 0) { - return this.http.authedRequest(callback, _httpApi.Method.Get, "/publicRooms"); + * @param limit - Maximum number of entries to return + * @param since - Token to paginate from + * @returns Promise which resolves: IPublicRoomsResponse + * @returns Rejects: with an error response. + */ + publicRooms(_ref = {}) { + let { + server, + limit, + since + } = _ref, + options = _objectWithoutProperties(_ref, _excluded); + const queryParams = { + server, + limit, + since + }; + if (Object.keys(options).length === 0) { + return this.http.authedRequest(_httpApi.Method.Get, "/publicRooms", queryParams); } else { - return this.http.authedRequest(callback, _httpApi.Method.Post, "/publicRooms", queryParams, options); + return this.http.authedRequest(_httpApi.Method.Post, "/publicRooms", queryParams, options); } } + /** * Create an alias to room ID mapping. - * @param {string} alias The room alias to create. - * @param {string} roomId The room ID to link the alias to. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param alias - The room alias to create. + * @param roomId - The room ID to link the alias to. + * @returns Promise which resolves: an empty object `{}` + * @returns Rejects: with an error response. */ - - - createAlias(alias, roomId, callback) { + createAlias(alias, roomId) { const path = utils.encodeUri("/directory/room/$alias", { $alias: alias }); const data = { room_id: roomId }; - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, data); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, data); } + /** * Delete an alias to room ID mapping. This alias must be on your local server, * and you must have sufficient access to do this operation. - * @param {string} alias The room alias to delete. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: an empty object {}. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param alias - The room alias to delete. + * @returns Promise which resolves: an empty object `{}`. + * @returns Rejects: with an error response. */ - - - deleteAlias(alias, callback) { + deleteAlias(alias) { const path = utils.encodeUri("/directory/room/$alias", { $alias: alias }); - return this.http.authedRequest(callback, _httpApi.Method.Delete, path); + return this.http.authedRequest(_httpApi.Method.Delete, path); } + /** * Gets the local aliases for the room. Note: this includes all local aliases, unlike the * curated list from the m.room.canonical_alias state event. - * @param {string} roomId The room ID to get local aliases for. - * @return {Promise} Resolves: an object with an `aliases` property, containing an array of local aliases - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param roomId - The room ID to get local aliases for. + * @returns Promise which resolves: an object with an `aliases` property, containing an array of local aliases + * @returns Rejects: with an error response. */ - - getLocalAliases(roomId) { const path = utils.encodeUri("/rooms/$roomId/aliases", { $roomId: roomId }); - const prefix = _httpApi.PREFIX_V3; - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, null, null, { + const prefix = _httpApi.ClientPrefix.V3; + return this.http.authedRequest(_httpApi.Method.Get, path, undefined, undefined, { prefix }); } + /** * Get room info for the given alias. - * @param {string} alias The room alias to resolve. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Object with room_id and servers. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param alias - The room alias to resolve. + * @returns Promise which resolves: Object with room_id and servers. + * @returns Rejects: with an error response. */ - - - getRoomIdForAlias(alias, callback) { + getRoomIdForAlias(alias) { // eslint-disable-line camelcase // TODO: deprecate this or resolveRoomAlias const path = utils.encodeUri("/directory/room/$alias", { $alias: alias }); - return this.http.authedRequest(callback, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** - * @param {string} roomAlias - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Object with room_id and servers. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Object with room_id and servers. + * @returns Rejects: with an error response. */ // eslint-disable-next-line camelcase - - - resolveRoomAlias(roomAlias, callback) { + resolveRoomAlias(roomAlias) { // TODO: deprecate this or getRoomIdForAlias const path = utils.encodeUri("/directory/room/$alias", { $alias: roomAlias }); - return this.http.request(callback, _httpApi.Method.Get, path); + return this.http.request(_httpApi.Method.Get, path); } + /** * Get the visibility of a room in the current HS's room directory - * @param {string} roomId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - getRoomDirectoryVisibility(roomId, callback) { + getRoomDirectoryVisibility(roomId) { const path = utils.encodeUri("/directory/list/room/$roomId", { $roomId: roomId }); - return this.http.authedRequest(callback, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** * Set the visbility of a room in the current HS's room directory - * @param {string} roomId - * @param {string} visibility "public" to make the room visible + * @param visibility - "public" to make the room visible * in the public directory, or "private" to make * it invisible. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - - setRoomDirectoryVisibility(roomId, visibility, callback) { + setRoomDirectoryVisibility(roomId, visibility) { const path = utils.encodeUri("/directory/list/room/$roomId", { $roomId: roomId }); - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, { + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, { visibility }); } + /** * Set the visbility of a room bridged to a 3rd party network in * the current HS's room directory. - * @param {string} networkId the network ID of the 3rd party + * @param networkId - the network ID of the 3rd party * instance under which this room is published under. - * @param {string} roomId - * @param {string} visibility "public" to make the room visible + * @param visibility - "public" to make the room visible * in the public directory, or "private" to make * it invisible. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: result object + * @returns Rejects: with an error response. */ - - - setRoomDirectoryVisibilityAppService(networkId, roomId, visibility, callback) { + setRoomDirectoryVisibilityAppService(networkId, roomId, visibility) { // TODO: Types const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", { $networkId: networkId, $roomId: roomId }); - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, { - "visibility": visibility + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, { + visibility: visibility }); } + /** * Query the user directory with a term matching user IDs, display names and domains. - * @param {object} opts options - * @param {string} opts.term the term with which to search. - * @param {number} opts.limit the maximum number of results to return. The server will + * @param term - the term with which to search. + * @param limit - the maximum number of results to return. The server will * apply a limit if unspecified. - * @return {Promise} Resolves: an array of results. + * @returns Promise which resolves: an array of results. */ - - - searchUserDirectory(opts) { + searchUserDirectory({ + term, + limit + }) { const body = { - search_term: opts.term + search_term: term }; - - if (opts.limit !== undefined) { - body.limit = opts.limit; + if (limit !== undefined) { + body.limit = limit; } - - return this.http.authedRequest(undefined, _httpApi.Method.Post, "/user_directory/search", undefined, body); + return this.http.authedRequest(_httpApi.Method.Post, "/user_directory/search", undefined, body); } + /** * Upload a file to the media repository on the homeserver. * - * @param {object} file The object to upload. On a browser, something that + * @param file - The object to upload. On a browser, something that * can be sent to XMLHttpRequest.send (typically a File). Under node.js, * a a Buffer, String or ReadStream. * - * @param {object} opts options object - * - * @param {string=} opts.name Name to give the file on the server. Defaults - * to file.name. - * - * @param {boolean=} opts.includeFilename if false will not send the filename, - * e.g for encrypted file uploads where filename leaks are undesirable. - * Defaults to true. - * - * @param {string=} opts.type Content-type for the upload. Defaults to - * file.type, or applicaton/octet-stream. - * - * @param {boolean=} opts.rawResponse Return the raw body, rather than - * parsing the JSON. Defaults to false (except on node.js, where it - * defaults to true for backwards compatibility). + * @param opts - options object * - * @param {boolean=} opts.onlyContentUri Just return the content URI, - * rather than the whole body. Defaults to false (except on browsers, - * where it defaults to true for backwards compatibility). Ignored if - * opts.rawResponse is true. - * - * @param {Function=} opts.callback Deprecated. Optional. The callback to - * invoke on success/failure. See the promise return values for more - * information. - * - * @param {Function=} opts.progressHandler Optional. Called when a chunk of - * data has been uploaded, with an object containing the fields `loaded` - * (number of bytes transferred) and `total` (total size, if known). - * - * @return {Promise} Resolves to response object, as + * @returns Promise which resolves to response object, as * determined by this.opts.onlyData, opts.rawResponse, and * opts.onlyContentUri. Rejects with an error (usually a MatrixError). */ - - uploadContent(file, opts) { return this.http.uploadContent(file, opts); } + /** * Cancel a file upload in progress - * @param {Promise} promise The promise returned from uploadContent - * @return {boolean} true if canceled, otherwise false + * @param upload - The object returned from uploadContent + * @returns true if canceled, otherwise false */ - - - cancelUpload(promise) { - return this.http.cancelUpload(promise); + cancelUpload(upload) { + return this.http.cancelUpload(upload); } + /** * Get a list of all file uploads in progress - * @return {array} Array of objects representing current uploads. + * @returns Array of objects representing current uploads. * Currently in progress is element 0. Keys: * - promise: The promise associated with the upload * - loaded: Number of bytes uploaded * - total: Total number of bytes to upload */ - - getCurrentUploads() { return this.http.getCurrentUploads(); } + /** - * @param {string} userId - * @param {string} info The kind of info to retrieve (e.g. 'displayname', + * @param info - The kind of info to retrieve (e.g. 'displayname', * 'avatar_url'). - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. */ - - - getProfileInfo(userId, info, callback // eslint-disable-next-line camelcase + getProfileInfo(userId, info + // eslint-disable-next-line camelcase ) { - if (utils.isFunction(info)) { - callback = info; // legacy - - info = undefined; - } - const path = info ? utils.encodeUri("/profile/$userId/$info", { $userId: userId, $info: info }) : utils.encodeUri("/profile/$userId", { $userId: userId }); - return this.http.authedRequest(callback, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves to a list of the user's threepids. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves to a list of the user's threepids. + * @returns Rejects: with an error response. */ - - - getThreePids(callback) { - return this.http.authedRequest(callback, _httpApi.Method.Get, "/account/3pid"); + getThreePids() { + return this.http.authedRequest(_httpApi.Method.Get, "/account/3pid"); } + /** * Add a 3PID to your homeserver account and optionally bind it to an identity * server as well. An identity server is required as part of the `creds` object. @@ -6958,23 +6296,19 @@ * This API is deprecated, and you should instead use `addThreePidOnly` * for homeservers that support it. * - * @param {Object} creds - * @param {boolean} bind - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: on success - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: on success + * @returns Rejects: with an error response. */ - - - addThreePid(creds, bind, callback) { + addThreePid(creds, bind) { // TODO: Types const path = "/account/3pid"; const data = { - 'threePidCreds': creds, - 'bind': bind + threePidCreds: creds, + bind: bind }; - return this.http.authedRequest(callback, _httpApi.Method.Post, path, null, data); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data); } + /** * Add a 3PID to your homeserver account. This API does not use an identity * server, as the homeserver is expected to handle 3PID ownership validation. @@ -6982,20 +6316,19 @@ * You can check whether a homeserver supports this API via * `doesServerSupportSeparateAddAndBind`. * - * @param {Object} data A object with 3PID validation data from having called + * @param data - A object with 3PID validation data from having called * `account/3pid//requestToken` on the homeserver. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - async addThreePidOnly(data) { const path = "/account/3pid/add"; - const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.PREFIX_R0 : _httpApi.PREFIX_UNSTABLE; - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, null, data, { + const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.ClientPrefix.R0 : _httpApi.ClientPrefix.Unstable; + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data, { prefix }); } + /** * Bind a 3PID for discovery onto an identity server via the homeserver. The * identity server handles 3PID ownership validation and the homeserver records @@ -7004,35 +6337,33 @@ * You can check whether a homeserver supports this API via * `doesServerSupportSeparateAddAndBind`. * - * @param {Object} data A object with 3PID validation data from having called + * @param data - A object with 3PID validation data from having called * `validate//requestToken` on the identity server. It should also * contain `id_server` and `id_access_token` fields as well. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - async bindThreePid(data) { const path = "/account/3pid/bind"; - const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.PREFIX_R0 : _httpApi.PREFIX_UNSTABLE; - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, null, data, { + const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.ClientPrefix.R0 : _httpApi.ClientPrefix.Unstable; + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data, { prefix }); } + /** * Unbind a 3PID for discovery on an identity server via the homeserver. The * homeserver removes its record of the binding to keep an updated record of * where all 3PIDs for the account are bound. * - * @param {string} medium The threepid medium (eg. 'email') - * @param {string} address The threepid address (eg. 'bob@example.com') + * @param medium - The threepid medium (eg. 'email') + * @param address - The threepid address (eg. 'bob\@example.com') * this must be as returned by getThreePids. - * @return {Promise} Resolves: on success - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: on success + * @returns Rejects: with an error response. */ - - - async unbindThreePid(medium, address // eslint-disable-next-line camelcase + async unbindThreePid(medium, address + // eslint-disable-next-line camelcase ) { const path = "/account/3pid/unbind"; const data = { @@ -7040,437 +6371,392 @@ address, id_server: this.getIdentityServerUrl(true) }; - const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.PREFIX_R0 : _httpApi.PREFIX_UNSTABLE; - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, null, data, { + const prefix = (await this.isVersionSupported("r0.6.0")) ? _httpApi.ClientPrefix.R0 : _httpApi.ClientPrefix.Unstable; + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data, { prefix }); } + /** - * @param {string} medium The threepid medium (eg. 'email') - * @param {string} address The threepid address (eg. 'bob@example.com') + * @param medium - The threepid medium (eg. 'email') + * @param address - The threepid address (eg. 'bob\@example.com') * this must be as returned by getThreePids. - * @return {Promise} Resolves: The server response on success + * @returns Promise which resolves: The server response on success * (generally the empty JSON object) - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Rejects: with an error response. */ - - - deleteThreePid(medium, address // eslint-disable-next-line camelcase + deleteThreePid(medium, address + // eslint-disable-next-line camelcase ) { const path = "/account/3pid/delete"; - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, null, { + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, { medium, address }); } + /** * Make a request to change your password. - * @param {Object} authDict - * @param {string} newPassword The new desired password. - * @param {boolean} logoutDevices Should all sessions be logged out after the password change. Defaults to true. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param newPassword - The new desired password. + * @param logoutDevices - Should all sessions be logged out after the password change. Defaults to true. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - - setPassword(authDict, newPassword, logoutDevices, callback) { - if (typeof logoutDevices === 'function') { - callback = logoutDevices; - } - - if (typeof logoutDevices !== 'boolean') { - // Use backwards compatible behaviour of not specifying logout_devices - // This way it is left up to the server: - logoutDevices = undefined; - } - + setPassword(authDict, newPassword, logoutDevices) { const path = "/account/password"; const data = { - 'auth': authDict, - 'new_password': newPassword, - 'logout_devices': logoutDevices + auth: authDict, + new_password: newPassword, + logout_devices: logoutDevices }; - return this.http.authedRequest(callback, _httpApi.Method.Post, path, null, data); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, data); } + /** * Gets all devices recorded for the logged-in user - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: result object + * @returns Rejects: with an error response. */ - - getDevices() { - return this.http.authedRequest(undefined, _httpApi.Method.Get, "/devices"); + return this.http.authedRequest(_httpApi.Method.Get, "/devices"); } + /** * Gets specific device details for the logged-in user - * @param {string} deviceId device to query - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param deviceId - device to query + * @returns Promise which resolves: result object + * @returns Rejects: with an error response. */ - - getDevice(deviceId) { const path = utils.encodeUri("/devices/$device_id", { $device_id: deviceId }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path); + return this.http.authedRequest(_httpApi.Method.Get, path); } + /** * Update the given device * - * @param {string} deviceId device to update - * @param {Object} body body of request - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param deviceId - device to update + * @param body - body of request + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ // eslint-disable-next-line camelcase - - setDeviceDetails(deviceId, body) { const path = utils.encodeUri("/devices/$device_id", { $device_id: deviceId }); - return this.http.authedRequest(undefined, _httpApi.Method.Put, path, undefined, body); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, body); } + /** * Delete the given device * - * @param {string} deviceId device to delete - * @param {object} auth Optional. Auth data to supply for User-Interactive auth. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param deviceId - device to delete + * @param auth - Optional. Auth data to supply for User-Interactive auth. + * @returns Promise which resolves: result object + * @returns Rejects: with an error response. */ - - deleteDevice(deviceId, auth) { const path = utils.encodeUri("/devices/$device_id", { $device_id: deviceId }); const body = {}; - if (auth) { body.auth = auth; } - - return this.http.authedRequest(undefined, _httpApi.Method.Delete, path, undefined, body); + return this.http.authedRequest(_httpApi.Method.Delete, path, undefined, body); } + /** * Delete multiple device * - * @param {string[]} devices IDs of the devices to delete - * @param {object} auth Optional. Auth data to supply for User-Interactive auth. - * @return {Promise} Resolves: result object - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param devices - IDs of the devices to delete + * @param auth - Optional. Auth data to supply for User-Interactive auth. + * @returns Promise which resolves: result object + * @returns Rejects: with an error response. */ - - deleteMultipleDevices(devices, auth) { const body = { devices }; - if (auth) { body.auth = auth; } - const path = "/delete_devices"; - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, undefined, body); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, body); } + /** * Gets all pushers registered for the logged-in user * - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Array of objects representing pushers - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Array of objects representing pushers + * @returns Rejects: with an error response. */ + async getPushers() { + const response = await this.http.authedRequest(_httpApi.Method.Get, "/pushers"); - - getPushers(callback) { - return this.http.authedRequest(callback, _httpApi.Method.Get, "/pushers"); + // Migration path for clients that connect to a homeserver that does not support + // MSC3881 yet, see https://github.com/matrix-org/matrix-spec-proposals/blob/kerry/remote-push-toggle/proposals/3881-remote-push-notification-toggling.md#migration + if (!(await this.doesServerSupportUnstableFeature("org.matrix.msc3881"))) { + response.pushers = response.pushers.map(pusher => { + if (!pusher.hasOwnProperty(_event2.PUSHER_ENABLED.name)) { + pusher[_event2.PUSHER_ENABLED.name] = true; + } + return pusher; + }); + } + return response; } + /** * Adds a new pusher or updates an existing pusher * - * @param {IPusherRequest} pusher Object representing a pusher - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: Empty json object on success - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @param pusher - Object representing a pusher + * @returns Promise which resolves: Empty json object on success + * @returns Rejects: with an error response. */ - - - setPusher(pusher, callback) { + setPusher(pusher) { const path = "/pushers/set"; - return this.http.authedRequest(callback, _httpApi.Method.Post, path, null, pusher); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, pusher); } + /** - * Get the push rules for the account from the server. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves to the push rules. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * Persists local notification settings + * @returns Promise which resolves: an empty object + * @returns Rejects: with an error response. */ + setLocalNotificationSettings(deviceId, notificationSettings) { + const key = `${_event2.LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`; + return this.setAccountData(key, notificationSettings); + } - - getPushRules(callback) { - return this.http.authedRequest(callback, _httpApi.Method.Get, "/pushrules/").then(rules => { - return _pushprocessor.PushProcessor.rewriteDefaultRules(rules); + /** + * Get the push rules for the account from the server. + * @returns Promise which resolves to the push rules. + * @returns Rejects: with an error response. + */ + getPushRules() { + return this.http.authedRequest(_httpApi.Method.Get, "/pushrules/").then(rules => { + this.setPushRules(rules); + return this.pushRules; }); } + /** - * @param {string} scope - * @param {string} kind - * @param {string} ruleId - * @param {Object} body - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * Update the push rules for the account. This should be called whenever + * updated push rules are available. */ + setPushRules(rules) { + // Fix-up defaults, if applicable. + this.pushRules = _pushprocessor.PushProcessor.rewriteDefaultRules(rules); + // Pre-calculate any necessary caches. + this.pushProcessor.updateCachedPushRuleKeys(this.pushRules); + } - - addPushRule(scope, kind, ruleId, body, callback) { + /** + * @returns Promise which resolves: an empty object `{}` + * @returns Rejects: with an error response. + */ + addPushRule(scope, kind, ruleId, body) { // NB. Scope not uri encoded because devices need the '/' const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { $kind: kind, $ruleId: ruleId }); - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, body); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, body); } + /** - * @param {string} scope - * @param {string} kind - * @param {string} ruleId - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: an empty object `{}` + * @returns Rejects: with an error response. */ - - - deletePushRule(scope, kind, ruleId, callback) { + deletePushRule(scope, kind, ruleId) { // NB. Scope not uri encoded because devices need the '/' const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", { $kind: kind, $ruleId: ruleId }); - return this.http.authedRequest(callback, _httpApi.Method.Delete, path); + return this.http.authedRequest(_httpApi.Method.Delete, path); } + /** * Enable or disable a push notification rule. - * @param {string} scope - * @param {string} kind - * @param {string} ruleId - * @param {boolean} enabled - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - - setPushRuleEnabled(scope, kind, ruleId, enabled, callback) { + setPushRuleEnabled(scope, kind, ruleId, enabled) { const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", { $kind: kind, $ruleId: ruleId }); - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, { - "enabled": enabled + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, { + enabled: enabled }); } + /** * Set the actions for a push notification rule. - * @param {string} scope - * @param {string} kind - * @param {string} ruleId - * @param {array} actions - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: to an empty object {} - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: to an empty object `{}` + * @returns Rejects: with an error response. */ - - - setPushRuleActions(scope, kind, ruleId, actions, callback) { + setPushRuleActions(scope, kind, ruleId, actions) { const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", { $kind: kind, $ruleId: ruleId }); - return this.http.authedRequest(callback, _httpApi.Method.Put, path, undefined, { - "actions": actions + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, { + actions: actions }); } + /** * Perform a server-side search. - * @param {Object} opts - * @param {string} opts.next_batch the batch token to pass in the query string - * @param {Object} opts.body the JSON object to pass to the request body. - * @param {module:client.callback} callback Optional. - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. - */ - - - search(opts, // eslint-disable-line camelcase - callback) { + * @param next_batch - the batch token to pass in the query string + * @param body - the JSON object to pass to the request body. + * @param abortSignal - optional signal used to cancel the http request. + * @returns Promise which resolves to the search response object. + * @returns Rejects: with an error response. + */ + search({ + body, + next_batch: nextBatch + }, abortSignal) { const queryParams = {}; - - if (opts.next_batch) { - queryParams.next_batch = opts.next_batch; + if (nextBatch) { + queryParams.next_batch = nextBatch; } - - return this.http.authedRequest(callback, _httpApi.Method.Post, "/search", queryParams, opts.body); + return this.http.authedRequest(_httpApi.Method.Post, "/search", queryParams, body, { + abortSignal + }); } + /** * Upload keys * - * @param {Object} content body of upload request + * @param content - body of upload request * - * @param {Object=} opts this method no longer takes any opts, + * @param opts - this method no longer takes any opts, * used to take opts.device_id but this was not removed from the spec as a redundant parameter * - * @param {module:client.callback=} callback - * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). + * @returns Promise which resolves: result object. Rejects: with + * an error response ({@link MatrixError}). */ - - - uploadKeysRequest(content, opts, callback) { - return this.http.authedRequest(callback, _httpApi.Method.Post, "/keys/upload", undefined, content); + uploadKeysRequest(content, opts) { + return this.http.authedRequest(_httpApi.Method.Post, "/keys/upload", undefined, content); } - uploadKeySignatures(content) { - return this.http.authedRequest(undefined, _httpApi.Method.Post, '/keys/signatures/upload', undefined, content, { - prefix: _httpApi.PREFIX_UNSTABLE + return this.http.authedRequest(_httpApi.Method.Post, "/keys/signatures/upload", undefined, content, { + prefix: _httpApi.ClientPrefix.V3 }); } + /** * Download device keys * - * @param {string[]} userIds list of users to get keys for + * @param userIds - list of users to get keys for * - * @param {Object=} opts - * - * @param {string=} opts.token sync token to pass in the query request, to help + * @param token - sync token to pass in the query request, to help * the HS give the most recent results * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). + * @returns Promise which resolves: result object. Rejects: with + * an error response ({@link MatrixError}). */ - - - downloadKeysForUsers(userIds, opts) { - if (utils.isFunction(opts)) { - // opts used to be 'callback'. - throw new Error('downloadKeysForUsers no longer accepts a callback parameter'); - } - - opts = opts || {}; + downloadKeysForUsers(userIds, { + token + } = {}) { const content = { device_keys: {} }; - - if ('token' in opts) { - content.token = opts.token; + if (token !== undefined) { + content.token = token; } - userIds.forEach(u => { content.device_keys[u] = []; }); - return this.http.authedRequest(undefined, _httpApi.Method.Post, "/keys/query", undefined, content); + return this.http.authedRequest(_httpApi.Method.Post, "/keys/query", undefined, content); } + /** * Claim one-time keys * - * @param {string[]} devices a list of [userId, deviceId] pairs + * @param devices - a list of [userId, deviceId] pairs * - * @param {string} [keyAlgorithm = signed_curve25519] desired key type + * @param keyAlgorithm - desired key type * - * @param {number} [timeout] the time (in milliseconds) to wait for keys from remote + * @param timeout - the time (in milliseconds) to wait for keys from remote * servers * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). + * @returns Promise which resolves: result object. Rejects: with + * an error response ({@link MatrixError}). */ - - claimOneTimeKeys(devices, keyAlgorithm = "signed_curve25519", timeout) { const queries = {}; - if (keyAlgorithm === undefined) { keyAlgorithm = "signed_curve25519"; } - - for (let i = 0; i < devices.length; ++i) { - const userId = devices[i][0]; - const deviceId = devices[i][1]; + for (const [userId, deviceId] of devices) { const query = queries[userId] || {}; queries[userId] = query; query[deviceId] = keyAlgorithm; } - const content = { one_time_keys: queries }; - if (timeout) { content.timeout = timeout; } - const path = "/keys/claim"; - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, undefined, content); + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, content); } + /** * Ask the server for a list of users who have changed their device lists * between a pair of sync tokens * - * @param {string} oldToken - * @param {string} newToken * - * @return {Promise} Resolves: result object. Rejects: with - * an error response ({@link module:http-api.MatrixError}). + * @returns Promise which resolves: result object. Rejects: with + * an error response ({@link MatrixError}). */ - - getKeyChanges(oldToken, newToken) { const qps = { from: oldToken, to: newToken }; - return this.http.authedRequest(undefined, _httpApi.Method.Get, "/keys/changes", qps); + return this.http.authedRequest(_httpApi.Method.Get, "/keys/changes", qps); } - uploadDeviceSigningKeys(auth, keys) { // API returns empty object const data = Object.assign({}, keys); if (auth) Object.assign(data, { auth }); - return this.http.authedRequest(undefined, _httpApi.Method.Post, "/keys/device_signing/upload", undefined, data, { - prefix: _httpApi.PREFIX_UNSTABLE + return this.http.authedRequest(_httpApi.Method.Post, "/keys/device_signing/upload", undefined, data, { + prefix: _httpApi.ClientPrefix.Unstable }); } + /** * Register with an identity server using the OpenID token from the user's * Homeserver, which can be retrieved via - * {@link module:client~MatrixClient#getOpenIdToken}. + * {@link MatrixClient#getOpenIdToken}. * * Note that the `/account/register` endpoint (as well as IS authentication in * general) was added as part of the v2 API version. * - * @param {object} hsOpenIdToken - * @return {Promise} Resolves: with object containing an Identity + * @returns Promise which resolves: with object containing an Identity * Server access token. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Rejects: with an error response. */ - - registerWithIdentityServer(hsOpenIdToken) { - // TODO: Types if (!this.idBaseUrl) { throw new Error("No identity server base URL set"); } - - const uri = this.idBaseUrl + _httpApi.PREFIX_IDENTITY_V2 + "/account/register"; - return this.http.requestOtherUrl(undefined, _httpApi.Method.Post, uri, null, hsOpenIdToken); + const uri = this.http.getUrl("/account/register", undefined, _httpApi.IdentityPrefix.V2, this.idBaseUrl); + return this.http.requestOtherUrl(_httpApi.Method.Post, uri, hsOpenIdToken); } + /** * Requests an email verification token directly from an identity server. * @@ -7478,35 +6764,34 @@ * server. The validation data that results should be passed to the * `bindThreePid` method to complete the binding process. * - * @param {string} email The email address to request a token for - * @param {string} clientSecret A secret binary string generated by the client. + * @param email - The email address to request a token for + * @param clientSecret - A secret binary string generated by the client. * It is recommended this be around 16 ASCII characters. - * @param {number} sendAttempt If an identity server sees a duplicate request + * @param sendAttempt - If an identity server sees a duplicate request * with the same sendAttempt, it will not send another email. * To request another email to be sent, use a larger value for * the sendAttempt param as was used in the previous request. - * @param {string} nextLink Optional If specified, the client will be redirected + * @param nextLink - Optional If specified, the client will be redirected * to this link after validation. - * @param {module:client.callback} callback Optional. - * @param {string} identityAccessToken The `access_token` field of the identity + * @param identityAccessToken - The `access_token` field of the identity * server `/account/register` response (see {@link registerWithIdentityServer}). * - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: TODO + * @returns Rejects: with an error response. * @throws Error if no identity server is set */ - - - requestEmailToken(email, clientSecret, sendAttempt, nextLink, callback, identityAccessToken) { - // TODO: Types + requestEmailToken(email, clientSecret, sendAttempt, nextLink, identityAccessToken) { const params = { client_secret: clientSecret, email: email, - send_attempt: sendAttempt?.toString(), - next_link: nextLink + send_attempt: sendAttempt?.toString() }; - return this.http.idServerRequest(callback, _httpApi.Method.Post, "/validate/email/requestToken", params, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); + if (nextLink) { + params.next_link = nextLink; + } + return this.http.idServerRequest(_httpApi.Method.Post, "/validate/email/requestToken", params, _httpApi.IdentityPrefix.V2, identityAccessToken); } + /** * Requests a MSISDN verification token directly from an identity server. * @@ -7514,39 +6799,38 @@ * server. The validation data that results should be passed to the * `bindThreePid` method to complete the binding process. * - * @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in + * @param phoneCountry - The ISO 3166-1 alpha-2 code for the country in * which phoneNumber should be parsed relative to. - * @param {string} phoneNumber The phone number, in national or international + * @param phoneNumber - The phone number, in national or international * format - * @param {string} clientSecret A secret binary string generated by the client. + * @param clientSecret - A secret binary string generated by the client. * It is recommended this be around 16 ASCII characters. - * @param {number} sendAttempt If an identity server sees a duplicate request + * @param sendAttempt - If an identity server sees a duplicate request * with the same sendAttempt, it will not send another SMS. * To request another SMS to be sent, use a larger value for * the sendAttempt param as was used in the previous request. - * @param {string} nextLink Optional If specified, the client will be redirected + * @param nextLink - Optional If specified, the client will be redirected * to this link after validation. - * @param {module:client.callback} callback Optional. - * @param {string} identityAccessToken The `access_token` field of the Identity + * @param identityAccessToken - The `access_token` field of the Identity * Server `/account/register` response (see {@link registerWithIdentityServer}). * - * @return {Promise} Resolves: TODO - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves to an object with a sid string + * @returns Rejects: with an error response. * @throws Error if no identity server is set */ - - - requestMsisdnToken(phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink, callback, identityAccessToken) { - // TODO: Types + requestMsisdnToken(phoneCountry, phoneNumber, clientSecret, sendAttempt, nextLink, identityAccessToken) { const params = { client_secret: clientSecret, country: phoneCountry, phone_number: phoneNumber, - send_attempt: sendAttempt?.toString(), - next_link: nextLink + send_attempt: sendAttempt?.toString() }; - return this.http.idServerRequest(callback, _httpApi.Method.Post, "/validate/msisdn/requestToken", params, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); + if (nextLink) { + params.next_link = nextLink; + } + return this.http.idServerRequest(_httpApi.Method.Post, "/validate/msisdn/requestToken", params, _httpApi.IdentityPrefix.V2, identityAccessToken); } + /** * Submits a MSISDN token to the identity server * @@ -7555,19 +6839,17 @@ * not expose this, since email is normally validated by the user clicking * a link rather than entering a code. * - * @param {string} sid The sid given in the response to requestToken - * @param {string} clientSecret A secret binary string generated by the client. + * @param sid - The sid given in the response to requestToken + * @param clientSecret - A secret binary string generated by the client. * This must be the same value submitted in the requestToken call. - * @param {string} msisdnToken The MSISDN token, as enetered by the user. - * @param {string} identityAccessToken The `access_token` field of the Identity + * @param msisdnToken - The MSISDN token, as enetered by the user. + * @param identityAccessToken - The `access_token` field of the Identity * Server `/account/register` response (see {@link registerWithIdentityServer}). * - * @return {Promise} Resolves: Object, currently with no parameters. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Object, currently with no parameters. + * @returns Rejects: with an error response. * @throws Error if No identity server is set */ - - submitMsisdnToken(sid, clientSecret, msisdnToken, identityAccessToken) { // TODO: Types const params = { @@ -7575,8 +6857,9 @@ client_secret: clientSecret, token: msisdnToken }; - return this.http.idServerRequest(undefined, _httpApi.Method.Post, "/validate/msisdn/submitToken", params, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); + return this.http.idServerRequest(_httpApi.Method.Post, "/validate/msisdn/submitToken", params, _httpApi.IdentityPrefix.V2, identityAccessToken); } + /** * Submits a MSISDN token to an arbitrary URL. * @@ -7586,17 +6869,15 @@ * `submit_url` to specify where the token should be sent, and this helper can * be used to pass the token to this URL. * - * @param {string} url The URL to submit the token to - * @param {string} sid The sid given in the response to requestToken - * @param {string} clientSecret A secret binary string generated by the client. + * @param url - The URL to submit the token to + * @param sid - The sid given in the response to requestToken + * @param clientSecret - A secret binary string generated by the client. * This must be the same value submitted in the requestToken call. - * @param {string} msisdnToken The MSISDN token, as enetered by the user. + * @param msisdnToken - The MSISDN token, as enetered by the user. * - * @return {Promise} Resolves: Object, currently with no parameters. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Object, currently with no parameters. + * @returns Rejects: with an error response. */ - - submitMsisdnTokenOtherUrl(url, sid, clientSecret, msisdnToken) { // TODO: Types const params = { @@ -7604,72 +6885,69 @@ client_secret: clientSecret, token: msisdnToken }; - return this.http.requestOtherUrl(undefined, _httpApi.Method.Post, url, undefined, params); + return this.http.requestOtherUrl(_httpApi.Method.Post, url, params); } + /** * Gets the V2 hashing information from the identity server. Primarily useful for * lookups. - * @param {string} identityAccessToken The access token for the identity server. - * @returns {Promise} The hashing information for the identity server. + * @param identityAccessToken - The access token for the identity server. + * @returns The hashing information for the identity server. */ - - getIdentityHashDetails(identityAccessToken) { // TODO: Types - return this.http.idServerRequest(undefined, _httpApi.Method.Get, "/hash_details", null, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); + return this.http.idServerRequest(_httpApi.Method.Get, "/hash_details", undefined, _httpApi.IdentityPrefix.V2, identityAccessToken); } + /** * Performs a hashed lookup of addresses against the identity server. This is * only supported on identity servers which have at least the version 2 API. - * @param {Array>} addressPairs An array of 2 element arrays. + * @param addressPairs - An array of 2 element arrays. * The first element of each pair is the address, the second is the 3PID medium. - * Eg: ["email@example.org", "email"] - * @param {string} identityAccessToken The access token for the identity server. - * @returns {Promise>} A collection of address mappings to + * Eg: `["email@example.org", "email"]` + * @param identityAccessToken - The access token for the identity server. + * @returns A collection of address mappings to * found MXIDs. Results where no user could be found will not be listed. */ - - async identityHashedLookup(addressPairs, identityAccessToken) { - const params = {// addresses: ["email@example.org", "10005550000"], + const params = { + // addresses: ["email@example.org", "10005550000"], // algorithm: "sha256", // pepper: "abc123" - }; // Get hash information first before trying to do a lookup + }; + // Get hash information first before trying to do a lookup const hashes = await this.getIdentityHashDetails(identityAccessToken); - - if (!hashes || !hashes['lookup_pepper'] || !hashes['algorithms']) { + if (!hashes || !hashes["lookup_pepper"] || !hashes["algorithms"]) { throw new Error("Unsupported identity server: bad response"); } - - params['pepper'] = hashes['lookup_pepper']; - const localMapping = {// hashed identifier => plain text address + params["pepper"] = hashes["lookup_pepper"]; + const localMapping = { + // hashed identifier => plain text address // For use in this function's return format - }; // When picking an algorithm, we pick the hashed over no hashes + }; - if (hashes['algorithms'].includes('sha256')) { + // When picking an algorithm, we pick the hashed over no hashes + if (hashes["algorithms"].includes("sha256")) { // Abuse the olm hashing const olmutil = new global.Olm.Utility(); params["addresses"] = addressPairs.map(p => { const addr = p[0].toLowerCase(); // lowercase to get consistent hashes - const med = p[1].toLowerCase(); - const hashed = olmutil.sha256(`${addr} ${med} ${params['pepper']}`).replace(/\+/g, '-').replace(/\//g, '_'); // URL-safe base64 + const hashed = olmutil.sha256(`${addr} ${med} ${params["pepper"]}`).replace(/\+/g, "-").replace(/\//g, "_"); // URL-safe base64 // Map the hash to a known (case-sensitive) address. We use the case // sensitive version because the caller might be expecting that. - localMapping[hashed] = p[0]; return hashed; }); params["algorithm"] = "sha256"; - } else if (hashes['algorithms'].includes('none')) { + } else if (hashes["algorithms"].includes("none")) { params["addresses"] = addressPairs.map(p => { const addr = p[0].toLowerCase(); // lowercase to get consistent hashes - const med = p[1].toLowerCase(); - const unhashed = `${addr} ${med}`; // Map the unhashed values to a known (case-sensitive) address. We use - // the case sensitive version because the caller might be expecting that. - + const unhashed = `${addr} ${med}`; + // Map the unhashed values to a known (case-sensitive) address. We use + // the case-sensitive version because the caller might be expecting that. localMapping[unhashed] = p[0]; return unhashed; }); @@ -7677,272 +6955,243 @@ } else { throw new Error("Unsupported identity server: unknown hash algorithm"); } + const response = await this.http.idServerRequest(_httpApi.Method.Post, "/lookup", params, _httpApi.IdentityPrefix.V2, identityAccessToken); + if (!response?.["mappings"]) return []; // no results - const response = await this.http.idServerRequest(undefined, _httpApi.Method.Post, "/lookup", params, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); - if (!response || !response['mappings']) return []; // no results - - const foundAddresses = [ - /* {address: "plain@example.org", mxid} */ - ]; - - for (const hashed of Object.keys(response['mappings'])) { - const mxid = response['mappings'][hashed]; + const foundAddresses = []; + for (const hashed of Object.keys(response["mappings"])) { + const mxid = response["mappings"][hashed]; const plainAddress = localMapping[hashed]; - if (!plainAddress) { throw new Error("Identity server returned more results than expected"); } - foundAddresses.push({ address: plainAddress, mxid }); } - return foundAddresses; } + /** * Looks up the public Matrix ID mapping for a given 3rd party * identifier from the identity server * - * @param {string} medium The medium of the threepid, eg. 'email' - * @param {string} address The textual address of the threepid - * @param {module:client.callback} callback Optional. - * @param {string} identityAccessToken The `access_token` field of the Identity + * @param medium - The medium of the threepid, eg. 'email' + * @param address - The textual address of the threepid + * @param identityAccessToken - The `access_token` field of the Identity * Server `/account/register` response (see {@link registerWithIdentityServer}). * - * @return {Promise} Resolves: A threepid mapping + * @returns Promise which resolves: A threepid mapping * object or the empty object if no mapping * exists - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Rejects: with an error response. */ - - - async lookupThreePid(medium, address, callback, identityAccessToken) { + async lookupThreePid(medium, address, identityAccessToken) { // TODO: Types // Note: we're using the V2 API by calling this function, but our // function contract requires a V1 response. We therefore have to // convert it manually. const response = await this.identityHashedLookup([[address, medium]], identityAccessToken); const result = response.find(p => p.address === address); - if (!result) { - if (callback) callback(null, {}); return {}; } - const mapping = { address, medium, - mxid: result.mxid // We can't reasonably fill these parameters: + mxid: result.mxid + + // We can't reasonably fill these parameters: // not_before // not_after // ts // signatures - }; - if (callback) callback(null, mapping); + return mapping; } + /** * Looks up the public Matrix ID mappings for multiple 3PIDs. * - * @param {Array.>} query Array of arrays containing + * @param query - Array of arrays containing * [medium, address] - * @param {string} identityAccessToken The `access_token` field of the Identity + * @param identityAccessToken - The `access_token` field of the Identity * Server `/account/register` response (see {@link registerWithIdentityServer}). * - * @return {Promise} Resolves: Lookup results from IS. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: Lookup results from IS. + * @returns Rejects: with an error response. */ - - async bulkLookupThreePids(query, identityAccessToken) { // TODO: Types // Note: we're using the V2 API by calling this function, but our // function contract requires a V1 response. We therefore have to // convert it manually. - const response = await this.identityHashedLookup( // We have to reverse the query order to get [address, medium] pairs + const response = await this.identityHashedLookup( + // We have to reverse the query order to get [address, medium] pairs query.map(p => [p[1], p[0]]), identityAccessToken); const v1results = []; - for (const mapping of response) { const originalQuery = query.find(p => p[1] === mapping.address); - if (!originalQuery) { throw new Error("Identity sever returned unexpected results"); } - - v1results.push([originalQuery[0], // medium + v1results.push([originalQuery[0], + // medium mapping.address, mapping.mxid]); } - return { threepids: v1results }; } + /** * Get account info from the identity server. This is useful as a neutral check * to verify that other APIs are likely to approve access by testing that the * token is valid, terms have been agreed, etc. * - * @param {string} identityAccessToken The `access_token` field of the Identity + * @param identityAccessToken - The `access_token` field of the Identity * Server `/account/register` response (see {@link registerWithIdentityServer}). * - * @return {Promise} Resolves: an object with account info. - * @return {module:http-api.MatrixError} Rejects: with an error response. + * @returns Promise which resolves: an object with account info. + * @returns Rejects: with an error response. */ - - getIdentityAccount(identityAccessToken) { // TODO: Types - return this.http.idServerRequest(undefined, _httpApi.Method.Get, "/account", undefined, _httpApi.PREFIX_IDENTITY_V2, identityAccessToken); + return this.http.idServerRequest(_httpApi.Method.Get, "/account", undefined, _httpApi.IdentityPrefix.V2, identityAccessToken); } + /** * Send an event to a specific list of devices. * This is a low-level API that simply wraps the HTTP API * call to send to-device messages. We recommend using * queueToDevice() which is a higher level API. * - * @param {string} eventType type of event to send - * @param {Object.>} contentMap + * @param eventType - type of event to send * content to send. Map from user_id to device_id to content object. - * @param {string=} txnId transaction id. One will be made up if not + * @param txnId - transaction id. One will be made up if not * supplied. - * @return {Promise} Resolves: to an empty object {} + * @returns Promise which resolves: to an empty object `{}` */ - - sendToDevice(eventType, contentMap, txnId) { const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", { $eventType: eventType, $txnId: txnId ? txnId : this.makeTxnId() }); const body = { - messages: contentMap + messages: utils.recursiveMapToObject(contentMap) }; - const targets = Object.keys(contentMap).reduce((obj, key) => { - obj[key] = Object.keys(contentMap[key]); - return obj; - }, {}); - + const targets = new Map(); + for (const [userId, deviceMessages] of contentMap) { + targets.set(userId, Array.from(deviceMessages.keys())); + } _logger.logger.log(`PUT ${path}`, targets); - - return this.http.authedRequest(undefined, _httpApi.Method.Put, path, undefined, body); + return this.http.authedRequest(_httpApi.Method.Put, path, undefined, body); } + /** * Sends events directly to specific devices using Matrix's to-device * messaging system. The batch will be split up into appropriately sized * batches for sending and stored in the store so they can be retried * later if they fail to send. Retries will happen automatically. - * @param batch The to-device messages to send + * @param batch - The to-device messages to send */ - - queueToDevice(batch) { return this.toDeviceMessageQueue.queueBatch(batch); } + /** * Get the third party protocols that can be reached using * this HS - * @return {Promise} Resolves to the result object + * @returns Promise which resolves to the result object */ - - getThirdpartyProtocols() { - return this.http.authedRequest(undefined, _httpApi.Method.Get, "/thirdparty/protocols").then(response => { + return this.http.authedRequest(_httpApi.Method.Get, "/thirdparty/protocols").then(response => { // sanity check - if (!response || typeof response !== 'object') { + if (!response || typeof response !== "object") { throw new Error(`/thirdparty/protocols did not return an object: ${response}`); } - return response; }); } + /** * Get information on how a specific place on a third party protocol * may be reached. - * @param {string} protocol The protocol given in getThirdpartyProtocols() - * @param {object} params Protocol-specific parameters, as given in the + * @param protocol - The protocol given in getThirdpartyProtocols() + * @param params - Protocol-specific parameters, as given in the * response to getThirdpartyProtocols() - * @return {Promise} Resolves to the result object + * @returns Promise which resolves to the result object */ - - getThirdpartyLocation(protocol, params) { const path = utils.encodeUri("/thirdparty/location/$protocol", { $protocol: protocol }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, params); + return this.http.authedRequest(_httpApi.Method.Get, path, params); } + /** * Get information on how a specific user on a third party protocol * may be reached. - * @param {string} protocol The protocol given in getThirdpartyProtocols() - * @param {object} params Protocol-specific parameters, as given in the + * @param protocol - The protocol given in getThirdpartyProtocols() + * @param params - Protocol-specific parameters, as given in the * response to getThirdpartyProtocols() - * @return {Promise} Resolves to the result object + * @returns Promise which resolves to the result object */ - - getThirdpartyUser(protocol, params) { // TODO: Types const path = utils.encodeUri("/thirdparty/user/$protocol", { $protocol: protocol }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, params); + return this.http.authedRequest(_httpApi.Method.Get, path, params); } - getTerms(serviceType, baseUrl) { // TODO: Types const url = this.termsUrlForService(serviceType, baseUrl); - return this.http.requestOtherUrl(undefined, _httpApi.Method.Get, url); + return this.http.requestOtherUrl(_httpApi.Method.Get, url); } - agreeToTerms(serviceType, baseUrl, accessToken, termsUrls) { - // TODO: Types const url = this.termsUrlForService(serviceType, baseUrl); const headers = { Authorization: "Bearer " + accessToken }; - return this.http.requestOtherUrl(undefined, _httpApi.Method.Post, url, null, { + return this.http.requestOtherUrl(_httpApi.Method.Post, url, { user_accepts: termsUrls }, { headers }); } + /** * Reports an event as inappropriate to the server, which may then notify the appropriate people. - * @param {string} roomId The room in which the event being reported is located. - * @param {string} eventId The event to report. - * @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive. - * @param {string} reason The reason the content is being reported. May be blank. - * @returns {Promise} Resolves to an empty object if successful + * @param roomId - The room in which the event being reported is located. + * @param eventId - The event to report. + * @param score - The score to rate this content as where -100 is most offensive and 0 is inoffensive. + * @param reason - The reason the content is being reported. May be blank. + * @returns Promise which resolves to an empty object if successful */ - - reportEvent(roomId, eventId, score, reason) { const path = utils.encodeUri("/rooms/$roomId/report/$eventId", { $roomId: roomId, $eventId: eventId }); - return this.http.authedRequest(undefined, _httpApi.Method.Post, path, null, { + return this.http.authedRequest(_httpApi.Method.Post, path, undefined, { score, reason }); } + /** * Fetches or paginates a room hierarchy as defined by MSC2946. * Falls back gracefully to sourcing its data from `getSpaceSummary` if this API is not yet supported by the server. - * @param {string} roomId The ID of the space-room to use as the root of the summary. - * @param {number?} limit The maximum number of rooms to return per page. - * @param {number?} maxDepth The maximum depth in the tree from the root room to return. - * @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true. - * @param {string?} fromToken The opaque token to paginate a previous request. - * @returns {Promise} the response, with next_batch & rooms fields. + * @param roomId - The ID of the space-room to use as the root of the summary. + * @param limit - The maximum number of rooms to return per page. + * @param maxDepth - The maximum depth in the tree from the root room to return. + * @param suggestedOnly - Whether to only return rooms with suggested=true. + * @param fromToken - The opaque token to paginate a previous request. + * @returns the response, with next_batch & rooms fields. */ - - getRoomHierarchy(roomId, limit, maxDepth, suggestedOnly = false, fromToken) { const path = utils.encodeUri("/rooms/$roomId/hierarchy", { $roomId: roomId @@ -7953,30 +7202,28 @@ from: fromToken, limit: limit?.toString() }; - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, queryParams, undefined, { - prefix: _httpApi.PREFIX_V1 + return this.http.authedRequest(_httpApi.Method.Get, path, queryParams, undefined, { + prefix: _httpApi.ClientPrefix.V1 }).catch(e => { if (e.errcode === "M_UNRECOGNIZED") { // fall back to the prefixed hierarchy API. - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, queryParams, undefined, { + return this.http.authedRequest(_httpApi.Method.Get, path, queryParams, undefined, { prefix: "/_matrix/client/unstable/org.matrix.msc2946" }); } - throw e; }); } + /** * Creates a new file tree space with the given name. The client will pick * defaults for how it expects to be able to support the remaining API offered * by the returned class. * * Note that this is UNSTABLE and may have breaking changes without notice. - * @param {string} name The name of the tree space. - * @returns {Promise} Resolves to the created space. + * @param name - The name of the tree space. + * @returns Promise which resolves to the created space. */ - - async unstableCreateFileTree(name) { const { room_id: roomId @@ -8007,19 +7254,18 @@ }); return new _MSC3089TreeSpace.MSC3089TreeSpace(this, roomId); } + /** * Gets a reference to a tree space, if the room ID given is a tree space. If the room * does not appear to be a tree space then null is returned. * * Note that this is UNSTABLE and may have breaking changes without notice. - * @param {string} roomId The room ID to get a tree space reference for. - * @returns {MSC3089TreeSpace} The tree space, or null if not a tree space. + * @param roomId - The room ID to get a tree space reference for. + * @returns The tree space, or null if not a tree space. */ - - unstableGetFileTreeSpace(roomId) { const room = this.getRoom(roomId); - if (room?.getMyMembership() !== 'join') return null; + if (room?.getMyMembership() !== "join") return null; const createEvent = room.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); const purposeEvent = room.currentState.getStateEvents(_event2.UNSTABLE_MSC3088_PURPOSE.name, _event2.UNSTABLE_MSC3089_TREE_SUBTYPE.name); if (!createEvent) throw new Error("Expected single room create event"); @@ -8027,440 +7273,218 @@ if (createEvent.getContent()?.[_event2.RoomCreateTypeField] !== _event2.RoomType.Space) return null; return new _MSC3089TreeSpace.MSC3089TreeSpace(this, roomId); } + /** * Perform a single MSC3575 sliding sync request. - * @param {MSC3575SlidingSyncRequest} req The request to make. - * @param {string} proxyBaseUrl The base URL for the sliding sync proxy. - * @returns {MSC3575SlidingSyncResponse} The sliding sync response, or a standard error. + * @param req - The request to make. + * @param proxyBaseUrl - The base URL for the sliding sync proxy. + * @param abortSignal - Optional signal to abort request mid-flight. + * @returns The sliding sync response, or a standard error. * @throws on non 2xx status codes with an object with a field "httpStatus":number. */ - - - slidingSync(req, proxyBaseUrl) { + slidingSync(req, proxyBaseUrl, abortSignal) { const qps = {}; - if (req.pos) { qps.pos = req.pos; delete req.pos; } - if (req.timeout) { qps.timeout = req.timeout; delete req.timeout; } - const clientTimeout = req.clientTimeout; delete req.clientTimeout; - return this.http.authedRequest(undefined, _httpApi.Method.Post, "/sync", qps, req, { + return this.http.authedRequest(_httpApi.Method.Post, "/sync", qps, req, { prefix: "/_matrix/client/unstable/org.matrix.msc3575", baseUrl: proxyBaseUrl, - localTimeoutMs: clientTimeout + localTimeoutMs: clientTimeout, + abortSignal }); } + /** - * @experimental + * @deprecated use supportsThreads() instead */ - - supportsExperimentalThreads() { + _logger.logger.warn(`supportsExperimentalThreads() is deprecated, use supportThreads() instead`); return this.clientOpts?.experimentalThreadSupport || false; } + + /** + * A helper to determine thread support + * @returns a boolean to determine if threads are enabled + */ + supportsThreads() { + return this.clientOpts?.threadSupport || false; + } + /** * Fetches the summary of a room as defined by an initial version of MSC3266 and implemented in Synapse * Proposed at https://github.com/matrix-org/matrix-doc/pull/3266 - * @param {string} roomIdOrAlias The ID or alias of the room to get the summary of. - * @param {string[]?} via The list of servers which know about the room if only an ID was provided. + * @param roomIdOrAlias - The ID or alias of the room to get the summary of. + * @param via - The list of servers which know about the room if only an ID was provided. */ - - async getRoomSummary(roomIdOrAlias, via) { const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, { + return this.http.authedRequest(_httpApi.Method.Get, path, { via - }, null, { - qsStringifyOptions: { - arrayFormat: 'repeat' - }, + }, undefined, { prefix: "/_matrix/client/unstable/im.nheko.summary" }); } + /** - * @experimental + * Processes a list of threaded events and adds them to their respective timelines + * @param room - the room the adds the threaded events + * @param threadedEvents - an array of the threaded events + * @param toStartOfTimeline - the direction in which we want to add the events */ - - processThreadEvents(room, threadedEvents, toStartOfTimeline) { room.processThreadedEvents(threadedEvents, toStartOfTimeline); } + /** + * Processes a list of thread roots and creates a thread model + * @param room - the room to create the threads in + * @param threadedEvents - an array of thread roots + * @param toStartOfTimeline - the direction + */ + processThreadRoots(room, threadedEvents, toStartOfTimeline) { + room.processThreadRoots(threadedEvents, toStartOfTimeline); + } processBeaconEvents(room, events) { + this.processAggregatedTimelineEvents(room, events); + } + + /** + * Calls aggregation functions for event types that are aggregated + * Polls and location beacons + * @param room - room the events belong to + * @param events - timeline events to be processed + * @returns + */ + processAggregatedTimelineEvents(room, events) { if (!events?.length) return; if (!room) return; room.currentState.processBeaconEvents(events, this); + room.processPollEvents(events); } + /** - * Fetches the user_id of the configured access token. + * Fetches information about the user for the configured access token. */ - - async whoami() { - // eslint-disable-line camelcase - return this.http.authedRequest(undefined, _httpApi.Method.Get, "/account/whoami"); + return this.http.authedRequest(_httpApi.Method.Get, "/account/whoami"); } + /** * Find the event_id closest to the given timestamp in the given direction. - * @return {Promise} A promise of an object containing the event_id and - * origin_server_ts of the closest event to the timestamp in the given - * direction + * @returns Resolves: A promise of an object containing the event_id and + * origin_server_ts of the closest event to the timestamp in the given direction + * @returns Rejects: when the request fails (module:http-api.MatrixError) */ - - - timestampToEvent(roomId, timestamp, dir) { + async timestampToEvent(roomId, timestamp, dir) { const path = utils.encodeUri("/rooms/$roomId/timestamp_to_event", { $roomId: roomId }); - return this.http.authedRequest(undefined, _httpApi.Method.Get, path, { + const queryParams = { ts: timestamp.toString(), dir: dir - }, undefined, { - prefix: "/_matrix/client/unstable/org.matrix.msc3030" - }); + }; + try { + return await this.http.authedRequest(_httpApi.Method.Get, path, queryParams, undefined, { + prefix: _httpApi.ClientPrefix.V1 + }); + } catch (err) { + // Fallback to the prefixed unstable endpoint. Since the stable endpoint is + // new, we should also try the unstable endpoint before giving up. We can + // remove this fallback request in a year (remove after 2023-11-28). + if (err.errcode === "M_UNRECOGNIZED" && ( + // XXX: The 400 status code check should be removed in the future + // when Synapse is compliant with MSC3743. + err.httpStatus === 400 || + // This the correct standard status code for an unsupported + // endpoint according to MSC3743. Not Found and Method Not Allowed + // both indicate that this endpoint+verb combination is + // not supported. + err.httpStatus === 404 || err.httpStatus === 405)) { + return await this.http.authedRequest(_httpApi.Method.Get, path, queryParams, undefined, { + prefix: "/_matrix/client/unstable/org.matrix.msc3030" + }); + } + throw err; + } } - } -/** - * Fires whenever the SDK receives a new event. - *

- * This is only fired for live events received via /sync - it is not fired for - * events received over context, search, or pagination APIs. - * - * @event module:client~MatrixClient#"event" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @example - * matrixClient.on("event", function(event){ - * var sender = event.getSender(); - * }); - */ - -/** - * Fires whenever the SDK receives a new to-device event. - * @event module:client~MatrixClient#"toDeviceEvent" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @example - * matrixClient.on("toDeviceEvent", function(event){ - * var sender = event.getSender(); - * }); - */ - -/** - * Fires whenever the SDK's syncing state is updated. The state can be one of: - *

    - * - *
  • PREPARED: The client has synced with the server at least once and is - * ready for methods to be called on it. This will be immediately followed by - * a state of SYNCING. This is the equivalent of "syncComplete" in the - * previous API.
  • - * - *
  • CATCHUP: The client has detected the connection to the server might be - * available again and will now try to do a sync again. As this sync might take - * a long time (depending how long ago was last synced, and general server - * performance) the client is put in this mode so the UI can reflect trying - * to catch up with the server after losing connection.
  • - * - *
  • SYNCING : The client is currently polling for new events from the server. - * This will be called after processing latest events from a sync.
  • - * - *
  • ERROR : The client has had a problem syncing with the server. If this is - * called before PREPARED then there was a problem performing the initial - * sync. If this is called after PREPARED then there was a problem polling - * the server for updates. This may be called multiple times even if the state is - * already ERROR. This is the equivalent of "syncError" in the previous - * API.
  • - * - *
  • RECONNECTING: The sync connection has dropped, but not (yet) in a way that - * should be considered erroneous. - *
  • - * - *
  • STOPPED: The client has stopped syncing with server due to stopClient - * being called. - *
  • - *
- * State transition diagram: - *
- *                                          +---->STOPPED
- *                                          |
- *              +----->PREPARED -------> SYNCING <--+
- *              |                        ^  |  ^    |
- *              |      CATCHUP ----------+  |  |    |
- *              |        ^                  V  |    |
- *   null ------+        |  +------- RECONNECTING   |
- *              |        V  V                       |
- *              +------->ERROR ---------------------+
- *
- * NB: 'null' will never be emitted by this event.
- *
- * 
- * Transitions: - *
    - * - *
  • null -> PREPARED : Occurs when the initial sync is completed - * first time. This involves setting up filters and obtaining push rules. - * - *
  • null -> ERROR : Occurs when the initial sync failed first time. - * - *
  • ERROR -> PREPARED : Occurs when the initial sync succeeds - * after previously failing. - * - *
  • PREPARED -> SYNCING : Occurs immediately after transitioning - * to PREPARED. Starts listening for live updates rather than catching up. - * - *
  • SYNCING -> RECONNECTING : Occurs when the live update fails. - * - *
  • RECONNECTING -> RECONNECTING : Can occur if the update calls - * continue to fail, but the keepalive calls (to /versions) succeed. - * - *
  • RECONNECTING -> ERROR : Occurs when the keepalive call also fails - * - *
  • ERROR -> SYNCING : Occurs when the client has performed a - * live update after having previously failed. - * - *
  • ERROR -> ERROR : Occurs when the client has failed to keepalive - * for a second time or more.
  • - * - *
  • SYNCING -> SYNCING : Occurs when the client has performed a live - * update. This is called after processing.
  • - * - *
  • * -> STOPPED : Occurs once the client has stopped syncing or - * trying to sync after stopClient has been called.
  • - *
- * - * @event module:client~MatrixClient#"sync" - * - * @param {string} state An enum representing the syncing state. One of "PREPARED", - * "SYNCING", "ERROR", "STOPPED". - * - * @param {?string} prevState An enum representing the previous syncing state. - * One of "PREPARED", "SYNCING", "ERROR", "STOPPED" or null. - * - * @param {?Object} data Data about this transition. - * - * @param {MatrixError} data.error The matrix error if state=ERROR. - * - * @param {String} data.oldSyncToken The 'since' token passed to /sync. - * null for the first successful sync since this client was - * started. Only present if state=PREPARED or - * state=SYNCING. - * - * @param {String} data.nextSyncToken The 'next_batch' result from /sync, which - * will become the 'since' token for the next call to /sync. Only present if - * state=PREPARED or state=SYNCING. - * - * @param {boolean} data.catchingUp True if we are working our way through a - * backlog of events after connecting. Only present if state=SYNCING. - * - * @example - * matrixClient.on("sync", function(state, prevState, data) { - * switch (state) { - * case "ERROR": - * // update UI to say "Connection Lost" - * break; - * case "SYNCING": - * // update UI to remove any "Connection Lost" message - * break; - * case "PREPARED": - * // the client instance is ready to be queried. - * var rooms = matrixClient.getRooms(); - * break; - * } - * }); - */ - -/** - * Fires whenever a new Room is added. This will fire when you are invited to a - * room, as well as when you join a room. This event is experimental and - * may change. - * @event module:client~MatrixClient#"Room" - * @param {Room} room The newly created, fully populated room. - * @example - * matrixClient.on("Room", function(room){ - * var roomId = room.roomId; - * }); - */ - -/** - * Fires whenever a Room is removed. This will fire when you forget a room. - * This event is experimental and may change. - * @event module:client~MatrixClient#"deleteRoom" - * @param {string} roomId The deleted room ID. - * @example - * matrixClient.on("deleteRoom", function(roomId){ - * // update UI from getRooms() - * }); - */ - -/** - * Fires whenever an incoming call arrives. - * @event module:client~MatrixClient#"Call.incoming" - * @param {module:webrtc/call~MatrixCall} call The incoming call. - * @example - * matrixClient.on("Call.incoming", function(call){ - * call.answer(); // auto-answer - * }); - */ - -/** - * Fires whenever the login session the JS SDK is using is no - * longer valid and the user must log in again. - * NB. This only fires when action is required from the user, not - * when then login session can be renewed by using a refresh token. - * @event module:client~MatrixClient#"Session.logged_out" - * @example - * matrixClient.on("Session.logged_out", function(errorObj){ - * // show the login screen - * }); - */ - -/** - * Fires when the JS SDK receives a M_CONSENT_NOT_GIVEN error in response - * to a HTTP request. - * @event module:client~MatrixClient#"no_consent" - * @example - * matrixClient.on("no_consent", function(message, contentUri) { - * console.info(message + ' Go to ' + contentUri); - * }); - */ - -/** - * Fires when a device is marked as verified/unverified/blocked/unblocked by - * {@link module:client~MatrixClient#setDeviceVerified|MatrixClient.setDeviceVerified} or - * {@link module:client~MatrixClient#setDeviceBlocked|MatrixClient.setDeviceBlocked}. - * - * @event module:client~MatrixClient#"deviceVerificationChanged" - * @param {string} userId the owner of the verified device - * @param {string} deviceId the id of the verified device - * @param {module:crypto/deviceinfo} deviceInfo updated device information - */ - -/** - * Fires when the trust status of a user changes - * If userId is the userId of the logged in user, this indicated a change - * in the trust status of the cross-signing data on the account. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @event module:client~MatrixClient#"userTrustStatusChanged" - * @param {string} userId the userId of the user in question - * @param {UserTrustLevel} trustLevel The new trust level of the user - */ - -/** - * Fires when the user's cross-signing keys have changed or cross-signing - * has been enabled/disabled. The client can use getStoredCrossSigningForUser - * with the user ID of the logged in user to check if cross-signing is - * enabled on the account. If enabled, it can test whether the current key - * is trusted using with checkUserTrust with the user ID of the logged - * in user. The checkOwnCrossSigningTrust function may be used to reconcile - * the trust in the account key. - * - * The cross-signing API is currently UNSTABLE and may change without notice. - * - * @event module:client~MatrixClient#"crossSigning.keysChanged" - */ /** - * Fires whenever new user-scoped account_data is added. - * @event module:client~MatrixClient#"accountData" - * @param {MatrixEvent} event The event describing the account_data just added - * @param {MatrixEvent} event The previous account data, if known. - * @example - * matrixClient.on("accountData", function(event, oldEvent){ - * myAccountData[event.type] = event.content; - * }); - */ - -/** - * Fires whenever the stored devices for a user have changed - * @event module:client~MatrixClient#"crypto.devicesUpdated" - * @param {String[]} users A list of user IDs that were updated - * @param {boolean} initialFetch If true, the store was empty (apart - * from our own device) and has been seeded. - */ - -/** - * Fires whenever the stored devices for a user will be updated - * @event module:client~MatrixClient#"crypto.willUpdateDevices" - * @param {String[]} users A list of user IDs that will be updated - * @param {boolean} initialFetch If true, the store is empty (apart - * from our own device) and is being seeded. - */ - -/** - * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() - * @event module:client~MatrixClient#"crypto.keyBackupStatus" - * @param {boolean} enabled true if key backup has been enabled, otherwise false - * @example - * matrixClient.on("crypto.keyBackupStatus", function(enabled){ - * if (enabled) { - * [...] - * } - * }); - */ - -/** - * Fires when we want to suggest to the user that they restore their megolm keys - * from backup or by cross-signing the device. - * - * @event module:client~MatrixClient#"crypto.suggestKeyRestore" - */ - -/** - * Fires when a key verification is requested. - * @event module:client~MatrixClient#"crypto.verification.request" - * @param {object} data - * @param {MatrixEvent} data.event the original verification request message - * @param {Array} data.methods the verification methods that can be used - * @param {Number} data.timeout the amount of milliseconds that should be waited - * before cancelling the request automatically. - * @param {Function} data.beginKeyVerification a function to call if a key - * verification should be performed. The function takes one argument: the - * name of the key verification method (taken from data.methods) to use. - * @param {Function} data.cancel a function to call if the key verification is - * rejected. - */ - -/** - * Fires when a key verification is requested with an unknown method. - * @event module:client~MatrixClient#"crypto.verification.request.unknown" - * @param {string} userId the user ID who requested the key verification - * @param {Function} cancel a function that will send a cancellation message to - * reject the key verification. - */ - -/** - * Fires when a secret request has been cancelled. If the client is prompting - * the user to ask whether they want to share a secret, the prompt can be - * dismissed. - * - * The Secure Secret Storage API is currently UNSTABLE and may change without notice. - * - * @event module:client~MatrixClient#"crypto.secrets.requestCancelled" - * @param {object} data - * @param {string} data.user_id The user ID of the client that had requested the secret. - * @param {string} data.device_id The device ID of the client that had requested the - * secret. - * @param {string} data.request_id The ID of the original request. - */ - -/** - * Fires when the client .well-known info is fetched. - * - * @event module:client~MatrixClient#"WellKnown.client" - * @param {object} data The JSON object returned by the server + * recalculates an accurate notifications count on event decryption. + * Servers do not have enough knowledge about encrypted events to calculate an + * accurate notification_count */ +exports.MatrixClient = MatrixClient; +_defineProperty(MatrixClient, "RESTORE_BACKUP_ERROR_BAD_KEY", "RESTORE_BACKUP_ERROR_BAD_KEY"); +function fixNotificationCountOnDecryption(cli, event) { + const ourUserId = cli.getUserId(); + const eventId = event.getId(); + const room = cli.getRoom(event.getRoomId()); + if (!room || !ourUserId || !eventId) return; + const oldActions = event.getPushActions(); + const actions = cli.getPushActionsForEvent(event, true); + const isThreadEvent = !!event.threadRootId && !event.isThreadRoot; + const currentHighlightCount = room.getUnreadCountForEventContext(_room.NotificationCountType.Highlight, event); + + // Ensure the unread counts are kept up to date if the event is encrypted + // We also want to make sure that the notification count goes up if we already + // have encrypted events to avoid other code from resetting 'highlight' to zero. + const oldHighlight = !!oldActions?.tweaks?.highlight; + const newHighlight = !!actions?.tweaks?.highlight; + let hasReadEvent; + if (isThreadEvent) { + const thread = room.getThread(event.threadRootId); + hasReadEvent = thread ? thread.hasUserReadEvent(ourUserId, eventId) : + // If the thread object does not exist in the room yet, we don't + // want to calculate notification for this event yet. We have not + // restored the read receipts yet and can't accurately calculate + // notifications at this stage. + // + // This issue can likely go away when MSC3874 is implemented + true; + } else { + hasReadEvent = room.hasUserReadEvent(ourUserId, eventId); + } + if (hasReadEvent) { + // If the event has been read, ignore it. + return; + } + if (oldHighlight !== newHighlight || currentHighlightCount > 0) { + // TODO: Handle mentions received while the client is offline + // See also https://github.com/vector-im/element-web/issues/9069 + let newCount = currentHighlightCount; + if (newHighlight && !oldHighlight) newCount++; + if (!newHighlight && oldHighlight) newCount--; + if (isThreadEvent) { + room.setThreadUnreadNotificationCount(event.threadRootId, _room.NotificationCountType.Highlight, newCount); + } else { + room.setUnreadNotificationCount(_room.NotificationCountType.Highlight, newCount); + } + } + // Total count is used to typically increment a room notification counter, but not loudly highlight it. + const currentTotalCount = room.getUnreadCountForEventContext(_room.NotificationCountType.Total, event); -exports.MatrixClient = MatrixClient; + // `notify` is used in practice for incrementing the total count + const newNotify = !!actions?.notify; -_defineProperty(MatrixClient, "RESTORE_BACKUP_ERROR_BAD_KEY", 'RESTORE_BACKUP_ERROR_BAD_KEY'); \ No newline at end of file + // The room total count is NEVER incremented by the server for encrypted rooms. We basically ignore + // the server here as it's always going to tell us to increment for encrypted events. + if (newNotify) { + if (isThreadEvent) { + room.setThreadUnreadNotificationCount(event.threadRootId, _room.NotificationCountType.Total, currentTotalCount + 1); + } else { + room.setUnreadNotificationCount(_room.NotificationCountType.Total, currentTotalCount + 1); + } + } +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/common-crypto/CryptoBackend.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/common-crypto/CryptoBackend.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/common-crypto/CryptoBackend.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/common-crypto/CryptoBackend.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/content-helpers.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/content-helpers.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/content-helpers.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/content-helpers.js 2023-04-11 06:11:52.000000000 +0000 @@ -12,28 +12,21 @@ exports.makeNotice = makeNotice; exports.makeTextMessage = makeTextMessage; exports.parseTopicContent = exports.parseLocationEvent = exports.parseBeaconInfoContent = exports.parseBeaconContent = exports.makeTopicContent = void 0; - -var _matrixEventsSdk = require("matrix-events-sdk"); - var _event = require("./@types/event"); - var _extensible_events = require("./@types/extensible_events"); - +var _utilities = require("./extensible_events_v1/utilities"); var _location = require("./@types/location"); - var _topic = require("./@types/topic"); - function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * Generates the content for a HTML Message event - * @param {string} body the plaintext body of the message - * @param {string} htmlBody the HTML representation of the message - * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + * @param body - the plaintext body of the message + * @param htmlBody - the HTML representation of the message + * @returns */ function makeHtmlMessage(body, htmlBody) { return { @@ -43,14 +36,13 @@ formatted_body: htmlBody }; } + /** * Generates the content for a HTML Notice event - * @param {string} body the plaintext body of the notice - * @param {string} htmlBody the HTML representation of the notice - * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + * @param body - the plaintext body of the notice + * @param htmlBody - the HTML representation of the notice + * @returns */ - - function makeHtmlNotice(body, htmlBody) { return { msgtype: _event.MsgType.Notice, @@ -59,14 +51,13 @@ formatted_body: htmlBody }; } + /** * Generates the content for a HTML Emote event - * @param {string} body the plaintext body of the emote - * @param {string} htmlBody the HTML representation of the emote - * @returns {{msgtype: string, format: string, body: string, formatted_body: string}} + * @param body - the plaintext body of the emote + * @param htmlBody - the HTML representation of the emote + * @returns */ - - function makeHtmlEmote(body, htmlBody) { return { msgtype: _event.MsgType.Emote, @@ -75,67 +66,61 @@ formatted_body: htmlBody }; } + /** * Generates the content for a Plaintext Message event - * @param {string} body the plaintext body of the emote - * @returns {{msgtype: string, body: string}} + * @param body - the plaintext body of the emote + * @returns */ - - function makeTextMessage(body) { return { msgtype: _event.MsgType.Text, body: body }; } + /** * Generates the content for a Plaintext Notice event - * @param {string} body the plaintext body of the notice - * @returns {{msgtype: string, body: string}} + * @param body - the plaintext body of the notice + * @returns */ - - function makeNotice(body) { return { msgtype: _event.MsgType.Notice, body: body }; } + /** * Generates the content for a Plaintext Emote event - * @param {string} body the plaintext body of the emote - * @returns {{msgtype: string, body: string}} + * @param body - the plaintext body of the emote + * @returns */ - - function makeEmoteMessage(body) { return { msgtype: _event.MsgType.Emote, body: body }; } -/** Location content helpers */ +/** Location content helpers */ const getTextForLocationEvent = (uri, assetType, timestamp, description) => { const date = `at ${new Date(timestamp).toISOString()}`; - const assetName = assetType === _location.LocationAssetType.Self ? 'User' : undefined; + const assetName = assetType === _location.LocationAssetType.Self ? "User" : undefined; const quotedDescription = description ? `"${description}"` : undefined; - return [assetName, 'Location', quotedDescription, uri, date].filter(Boolean).join(' '); + return [assetName, "Location", quotedDescription, uri, date].filter(Boolean).join(" "); }; + /** * Generates the content for a Location event - * @param uri a geo:// uri for the location - * @param timestamp the timestamp when the location was correct (milliseconds since - * the UNIX epoch) - * @param description the (optional) label for this location on the map - * @param assetType the (optional) asset type of this location e.g. "m.self" - * @param text optional. A text for the location + * @param uri - a geo:// uri for the location + * @param timestamp - the timestamp when the location was correct (milliseconds since the UNIX epoch) + * @param description - the (optional) label for this location on the map + * @param assetType - the (optional) asset type of this location e.g. "m.self" + * @param text - optional. A text for the location */ - - exports.getTextForLocationEvent = getTextForLocationEvent; - const makeLocationContent = (text, uri, timestamp, description, assetType) => { const defaultedText = text ?? getTextForLocationEvent(uri, assetType || _location.LocationAssetType.Self, timestamp, description); const timestampEvent = timestamp ? { @@ -152,77 +137,67 @@ [_location.M_ASSET.name]: { type: assetType || _location.LocationAssetType.Self }, - [_extensible_events.TEXT_NODE_TYPE.name]: defaultedText + [_extensible_events.M_TEXT.name]: defaultedText }, timestampEvent); }; + /** * Parse location event content and transform to * a backwards compatible modern m.location event format */ - - exports.makeLocationContent = makeLocationContent; - const parseLocationEvent = wireEventContent => { const location = _location.M_LOCATION.findIn(wireEventContent); - const asset = _location.M_ASSET.findIn(wireEventContent); - const timestamp = _location.M_TIMESTAMP.findIn(wireEventContent); - - const text = _extensible_events.TEXT_NODE_TYPE.findIn(wireEventContent); - + const text = _extensible_events.M_TEXT.findIn(wireEventContent); const geoUri = location?.uri ?? wireEventContent?.geo_uri; const description = location?.description; const assetType = asset?.type ?? _location.LocationAssetType.Self; const fallbackText = text ?? wireEventContent.body; - return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType); + return makeLocationContent(fallbackText, geoUri, timestamp ?? undefined, description, assetType); }; + /** * Topic event helpers */ - - exports.parseLocationEvent = parseLocationEvent; - const makeTopicContent = (topic, htmlTopic) => { const renderings = [{ body: topic, mimetype: "text/plain" }]; - - if ((0, _matrixEventsSdk.isProvided)(htmlTopic)) { + if ((0, _utilities.isProvided)(htmlTopic)) { renderings.push({ body: htmlTopic, mimetype: "text/html" }); } - return { topic, [_topic.M_TOPIC.name]: renderings }; }; - exports.makeTopicContent = makeTopicContent; - const parseTopicContent = content => { const mtopic = _topic.M_TOPIC.findIn(content); - - const text = mtopic?.find(r => !(0, _matrixEventsSdk.isProvided)(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic; + if (!Array.isArray(mtopic)) { + return { + text: content.topic + }; + } + const text = mtopic?.find(r => !(0, _utilities.isProvided)(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic; const html = mtopic?.find(r => r.mimetype === "text/html")?.body; return { text, html }; }; + /** * Beacon event helpers */ - - exports.parseTopicContent = parseTopicContent; - const makeBeaconInfoContent = (timeout, isLive, description, assetType, timestamp) => ({ description, timeout, @@ -232,9 +207,7 @@ type: assetType ?? _location.LocationAssetType.Self } }); - exports.makeBeaconInfoContent = makeBeaconInfoContent; - /** * Flatten beacon info event content */ @@ -244,11 +217,8 @@ timeout, live } = content; - - const timestamp = _location.M_TIMESTAMP.findIn(content); - + const timestamp = _location.M_TIMESTAMP.findIn(content) ?? undefined; const asset = _location.M_ASSET.findIn(content); - return { description, timeout, @@ -257,9 +227,7 @@ timestamp }; }; - exports.parseBeaconInfoContent = parseBeaconInfoContent; - const makeBeaconContent = (uri, timestamp, beaconInfoEventId, description) => ({ [_location.M_LOCATION.name]: { description, @@ -267,23 +235,18 @@ }, [_location.M_TIMESTAMP.name]: timestamp, "m.relates_to": { - rel_type: _matrixEventsSdk.REFERENCE_RELATION.name, + rel_type: _extensible_events.REFERENCE_RELATION.name, event_id: beaconInfoEventId } }); - exports.makeBeaconContent = makeBeaconContent; - const parseBeaconContent = content => { const location = _location.M_LOCATION.findIn(content); - - const timestamp = _location.M_TIMESTAMP.findIn(content); - + const timestamp = _location.M_TIMESTAMP.findIn(content) ?? undefined; return { description: location?.description, uri: location?.uri, timestamp }; }; - exports.parseBeaconContent = parseBeaconContent; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/content-repo.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/content-repo.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/content-repo.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/content-repo.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,13 +4,9 @@ value: true }); exports.getHttpUriForMxc = getHttpUriForMxc; - var utils = _interopRequireWildcard(require("./utils")); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - /* Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. @@ -28,67 +24,53 @@ */ /** - * @module content-repo - */ - -/** * Get the HTTP URL for an MXC URI. - * @param {string} baseUrl The base homeserver url which has a content repo. - * @param {string} mxc The mxc:// URI. - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param baseUrl - The base homeserver url which has a content repo. + * @param mxc - The mxc:// URI. + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {Boolean} allowDirectLinks If true, return any non-mxc URLs + * @param allowDirectLinks - If true, return any non-mxc URLs * directly. Fetching such URLs will leak information about the user to * anyone they share a room with. If false, will return the emptry string * for such URLs. - * @return {string} The complete URL to the content. + * @returns The complete URL to the content. */ function getHttpUriForMxc(baseUrl, mxc, width, height, resizeMethod, allowDirectLinks = false) { if (typeof mxc !== "string" || !mxc) { - return ''; + return ""; } - if (mxc.indexOf("mxc://") !== 0) { if (allowDirectLinks) { return mxc; } else { - return ''; + return ""; } } - let serverAndMediaId = mxc.slice(6); // strips mxc:// - let prefix = "/_matrix/media/r0/download/"; const params = {}; - if (width) { params["width"] = Math.round(width).toString(); } - if (height) { params["height"] = Math.round(height).toString(); } - if (resizeMethod) { params["method"] = resizeMethod; } - if (Object.keys(params).length > 0) { // these are thumbnailing params so they probably want the // thumbnailing API... prefix = "/_matrix/media/r0/thumbnail/"; } - const fragmentOffset = serverAndMediaId.indexOf("#"); let fragment = ""; - if (fragmentOffset >= 0) { fragment = serverAndMediaId.slice(fragmentOffset); serverAndMediaId = serverAndMediaId.slice(0, fragmentOffset); } - const urlParams = Object.keys(params).length === 0 ? "" : "?" + utils.encodeParams(params); return baseUrl + prefix + serverAndMediaId + urlParams + fragment; } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/aes.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/aes.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/aes.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/aes.js 2023-04-11 06:11:52.000000000 +0000 @@ -6,11 +6,8 @@ exports.calculateKeyCheck = calculateKeyCheck; exports.decryptAES = decryptAES; exports.encryptAES = encryptAES; - -var _utils = require("../utils"); - var _olmlib = require("./olmlib"); - +var _crypto = require("./crypto"); /* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. @@ -26,119 +23,39 @@ See the License for the specific language governing permissions and limitations under the License. */ -const subtleCrypto = typeof window !== "undefined" && window.crypto ? window.crypto.subtle || window.crypto.webkitSubtle : null; // salt for HKDF, with 8 bytes of zeros +// salt for HKDF, with 8 bytes of zeros const zeroSalt = new Uint8Array(8); - /** - * encrypt a string in Node.js + * encrypt a string * - * @param {string} data the plaintext to encrypt - * @param {Uint8Array} key the encryption key to use - * @param {string} name the name of the secret - * @param {string} ivStr the initialization vector to use + * @param data - the plaintext to encrypt + * @param key - the encryption key to use + * @param name - the name of the secret + * @param ivStr - the initialization vector to use */ -async function encryptNode(data, key, name, ivStr) { - const crypto = (0, _utils.getCrypto)(); - - if (!crypto) { - throw new Error("No usable crypto implementation"); - } - +async function encryptAES(data, key, name, ivStr) { let iv; - - if (ivStr) { - iv = (0, _olmlib.decodeBase64)(ivStr); - } else { - iv = crypto.randomBytes(16); // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary - // (which would mean we wouldn't be able to decrypt on Android). The loss - // of a single bit of iv is a price we have to pay. - - iv[8] &= 0x7f; - } - - const [aesKey, hmacKey] = deriveKeysNode(key, name); - const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv); - const ciphertext = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]); - const hmac = crypto.createHmac("sha256", hmacKey).update(ciphertext).digest("base64"); - return { - iv: (0, _olmlib.encodeBase64)(iv), - ciphertext: ciphertext.toString("base64"), - mac: hmac - }; -} -/** - * decrypt a string in Node.js - * - * @param {object} data the encrypted data - * @param {string} data.ciphertext the ciphertext in base64 - * @param {string} data.iv the initialization vector in base64 - * @param {string} data.mac the HMAC in base64 - * @param {Uint8Array} key the encryption key to use - * @param {string} name the name of the secret - */ - - -async function decryptNode(data, key, name) { - const crypto = (0, _utils.getCrypto)(); - - if (!crypto) { - throw new Error("No usable crypto implementation"); - } - - const [aesKey, hmacKey] = deriveKeysNode(key, name); - const hmac = crypto.createHmac("sha256", hmacKey).update(Buffer.from(data.ciphertext, "base64")).digest("base64").replace(/=+$/g, ''); - - if (hmac !== data.mac.replace(/=+$/g, '')) { - throw new Error(`Error decrypting secret ${name}: bad MAC`); - } - - const decipher = crypto.createDecipheriv("aes-256-ctr", aesKey, (0, _olmlib.decodeBase64)(data.iv)); - return decipher.update(data.ciphertext, "base64", "utf8") + decipher.final("utf8"); -} - -function deriveKeysNode(key, name) { - const crypto = (0, _utils.getCrypto)(); - const prk = crypto.createHmac("sha256", zeroSalt).update(key).digest(); - const b = Buffer.alloc(1, 1); - const aesKey = crypto.createHmac("sha256", prk).update(name, "utf8").update(b).digest(); - b[0] = 2; - const hmacKey = crypto.createHmac("sha256", prk).update(aesKey).update(name, "utf8").update(b).digest(); - return [aesKey, hmacKey]; -} -/** - * encrypt a string in Node.js - * - * @param {string} data the plaintext to encrypt - * @param {Uint8Array} key the encryption key to use - * @param {string} name the name of the secret - * @param {string} ivStr the initialization vector to use - */ - - -async function encryptBrowser(data, key, name, ivStr) { - let iv; - if (ivStr) { iv = (0, _olmlib.decodeBase64)(ivStr); } else { iv = new Uint8Array(16); - window.crypto.getRandomValues(iv); // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary + _crypto.crypto.getRandomValues(iv); + + // clear bit 63 of the IV to stop us hitting the 64-bit counter boundary // (which would mean we wouldn't be able to decrypt on Android). The loss // of a single bit of iv is a price we have to pay. - iv[8] &= 0x7f; } - - const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); - const encodedData = new TextEncoder().encode(data); - const ciphertext = await subtleCrypto.encrypt({ + const [aesKey, hmacKey] = await deriveKeys(key, name); + const encodedData = new _crypto.TextEncoder().encode(data); + const ciphertext = await _crypto.subtleCrypto.encrypt({ name: "AES-CTR", counter: iv, length: 64 }, aesKey, encodedData); - const hmac = await subtleCrypto.sign({ - name: 'HMAC' + const hmac = await _crypto.subtleCrypto.sign({ + name: "HMAC" }, hmacKey, ciphertext); return { iv: (0, _olmlib.encodeBase64)(iv), @@ -146,80 +63,65 @@ mac: (0, _olmlib.encodeBase64)(hmac) }; } + /** - * decrypt a string in the browser + * decrypt a string * - * @param {object} data the encrypted data - * @param {string} data.ciphertext the ciphertext in base64 - * @param {string} data.iv the initialization vector in base64 - * @param {string} data.mac the HMAC in base64 - * @param {Uint8Array} key the encryption key to use - * @param {string} name the name of the secret + * @param data - the encrypted data + * @param key - the encryption key to use + * @param name - the name of the secret */ - - -async function decryptBrowser(data, key, name) { - const [aesKey, hmacKey] = await deriveKeysBrowser(key, name); +async function decryptAES(data, key, name) { + const [aesKey, hmacKey] = await deriveKeys(key, name); const ciphertext = (0, _olmlib.decodeBase64)(data.ciphertext); - - if (!(await subtleCrypto.verify({ + if (!(await _crypto.subtleCrypto.verify({ name: "HMAC" }, hmacKey, (0, _olmlib.decodeBase64)(data.mac), ciphertext))) { throw new Error(`Error decrypting secret ${name}: bad MAC`); } - - const plaintext = await subtleCrypto.decrypt({ + const plaintext = await _crypto.subtleCrypto.decrypt({ name: "AES-CTR", counter: (0, _olmlib.decodeBase64)(data.iv), length: 64 }, aesKey, ciphertext); return new TextDecoder().decode(new Uint8Array(plaintext)); } - -async function deriveKeysBrowser(key, name) { - const hkdfkey = await subtleCrypto.importKey('raw', key, { +async function deriveKeys(key, name) { + const hkdfkey = await _crypto.subtleCrypto.importKey("raw", key, { name: "HKDF" }, false, ["deriveBits"]); - const keybits = await subtleCrypto.deriveBits({ + const keybits = await _crypto.subtleCrypto.deriveBits({ name: "HKDF", salt: zeroSalt, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879 - info: new TextEncoder().encode(name), + info: new _crypto.TextEncoder().encode(name), hash: "SHA-256" }, hkdfkey, 512); const aesKey = keybits.slice(0, 32); const hmacKey = keybits.slice(32); - const aesProm = subtleCrypto.importKey('raw', aesKey, { - name: 'AES-CTR' - }, false, ['encrypt', 'decrypt']); - const hmacProm = subtleCrypto.importKey('raw', hmacKey, { - name: 'HMAC', + const aesProm = _crypto.subtleCrypto.importKey("raw", aesKey, { + name: "AES-CTR" + }, false, ["encrypt", "decrypt"]); + const hmacProm = _crypto.subtleCrypto.importKey("raw", hmacKey, { + name: "HMAC", hash: { - name: 'SHA-256' + name: "SHA-256" } - }, false, ['sign', 'verify']); + }, false, ["sign", "verify"]); return Promise.all([aesProm, hmacProm]); } -function encryptAES(data, key, name, ivStr) { - return subtleCrypto ? encryptBrowser(data, key, name, ivStr) : encryptNode(data, key, name, ivStr); -} - -function decryptAES(data, key, name) { - return subtleCrypto ? decryptBrowser(data, key, name) : decryptNode(data, key, name); -} // string of zeroes, for calculating the key check - - +// string of zeroes, for calculating the key check const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + /** Calculate the MAC for checking the key. * - * @param {Uint8Array} key the key to use - * @param {string} [iv] The initialization vector as a base64-encoded string. + * @param key - the key to use + * @param iv - The initialization vector as a base64-encoded string. * If omitted, a random initialization vector will be created. - * @return {Promise} An object that contains, `mac` and `iv` properties. + * @returns An object that contains, `mac` and `iv` properties. */ - function calculateKeyCheck(key, iv) { return encryptAES(ZERO_STR, key, "", iv); } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/base.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/base.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/base.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/base.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,9 +5,9 @@ }); exports.UnknownDeviceError = exports.EncryptionAlgorithm = exports.ENCRYPTION_CLASSES = exports.DecryptionError = exports.DecryptionAlgorithm = exports.DECRYPTION_CLASSES = void 0; exports.registerAlgorithm = registerAlgorithm; - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. @@ -26,56 +26,32 @@ /** * Internal module. Defines the base classes of the encryption implementations - * - * @module */ /** - * map of registered encryption algorithm classes. A map from string to {@link - * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class - * - * @type {Object.} + * Map of registered encryption algorithm classes. A map from string to {@link EncryptionAlgorithm} class */ const ENCRYPTION_CLASSES = new Map(); exports.ENCRYPTION_CLASSES = ENCRYPTION_CLASSES; - /** - * map of registered encryption algorithm classes. Map from string to {@link - * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class - * - * @type {Object.} + * map of registered encryption algorithm classes. Map from string to {@link DecryptionAlgorithm} class */ const DECRYPTION_CLASSES = new Map(); exports.DECRYPTION_CLASSES = DECRYPTION_CLASSES; - /** * base type for encryption implementations - * - * @alias module:crypto/algorithms/base.EncryptionAlgorithm - * - * @param {object} params parameters - * @param {string} params.userId The UserID for the local user - * @param {string} params.deviceId The identifier for this device. - * @param {module:crypto} params.crypto crypto core - * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper - * @param {MatrixClient} baseApis base matrix api interface - * @param {string} params.roomId The ID of the room we will be sending to - * @param {object} params.config The body of the m.room.encryption event */ class EncryptionAlgorithm { + /** + * @param params - parameters + */ constructor(params) { _defineProperty(this, "userId", void 0); - _defineProperty(this, "deviceId", void 0); - _defineProperty(this, "crypto", void 0); - _defineProperty(this, "olmDevice", void 0); - _defineProperty(this, "baseApis", void 0); - _defineProperty(this, "roomId", void 0); - this.userId = params.userId; this.deviceId = params.deviceId; this.crypto = params.crypto; @@ -83,223 +59,167 @@ this.baseApis = params.baseApis; this.roomId = params.roomId; } + /** * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. * - * @param {module:models/room} room the room the event is in + * @param room - the room the event is in */ - - prepareToEncrypt(room) {} + /** * Encrypt a message event * - * @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage * @public - * @abstract * - * @param {module:models/room} room - * @param {string} eventType - * @param {object} content event content + * @param content - event content * - * @return {Promise} Promise which resolves to the new event body + * @returns Promise which resolves to the new event body */ - /** * Called when the membership of a member of the room changes. * - * @param {module:models/event.MatrixEvent} event event causing the change - * @param {module:models/room-member} member user whose membership changed - * @param {string=} oldMembership previous membership + * @param event - event causing the change + * @param member - user whose membership changed + * @param oldMembership - previous membership * @public - * @abstract */ onRoomMembership(event, member, oldMembership) {} - } + /** * base type for decryption implementations - * - * @alias module:crypto/algorithms/base.DecryptionAlgorithm - * @param {object} params parameters - * @param {string} params.userId The UserID for the local user - * @param {module:crypto} params.crypto crypto core - * @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper - * @param {MatrixClient} baseApis base matrix api interface - * @param {string=} params.roomId The ID of the room we will be receiving - * from. Null for to-device events. */ - - exports.EncryptionAlgorithm = EncryptionAlgorithm; - class DecryptionAlgorithm { constructor(params) { _defineProperty(this, "userId", void 0); - _defineProperty(this, "crypto", void 0); - _defineProperty(this, "olmDevice", void 0); - _defineProperty(this, "baseApis", void 0); - _defineProperty(this, "roomId", void 0); - this.userId = params.userId; this.crypto = params.crypto; this.olmDevice = params.olmDevice; this.baseApis = params.baseApis; this.roomId = params.roomId; } + /** * Decrypt an event * - * @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent - * @abstract - * - * @param {MatrixEvent} event undecrypted event + * @param event - undecrypted event * - * @return {Promise} promise which + * @returns promise which * resolves once we have finished decrypting. Rejects with an * `algorithms.DecryptionError` if there is a problem decrypting the event. */ - /** * Handle a key event * - * @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent - * - * @param {module:models/event.MatrixEvent} params event key event + * @param params - event key event */ - async onRoomKeyEvent(params) {// ignore by default + async onRoomKeyEvent(params) { + // ignore by default } + /** * Import a room key * - * @param {module:crypto/OlmDevice.MegolmSessionData} session - * @param {object} opts object + * @param opts - object */ - - - async importRoomKey(session, opts) {// ignore by default + async importRoomKey(session, opts) { + // ignore by default } + /** * Determine if we have the keys necessary to respond to a room key request * - * @param {module:crypto~IncomingRoomKeyRequest} keyRequest - * @return {Promise} true if we have the keys and could (theoretically) share + * @returns true if we have the keys and could (theoretically) share * them; else false. */ - - hasKeysForKeyRequest(keyRequest) { return Promise.resolve(false); } + /** * Send the response to a room key request * - * @param {module:crypto~IncomingRoomKeyRequest} keyRequest */ - - shareKeysWithDevice(keyRequest) { throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm"); } + /** * Retry decrypting all the events from a sender that haven't been * decrypted yet. * - * @param {string} senderKey the sender's key + * @param senderKey - the sender's key */ - - async retryDecryptionFromSender(senderKey) { // ignore by default return false; } - } + /** * Exception thrown when decryption fails * - * @alias module:crypto/algorithms/base.DecryptionError - * @param {string} msg user-visible message describing the problem + * @param msg - user-visible message describing the problem * - * @param {Object=} details key/value pairs reported in the logs but not shown + * @param details - key/value pairs reported in the logs but not shown * to the user. - * - * @extends Error */ - - exports.DecryptionAlgorithm = DecryptionAlgorithm; - class DecryptionError extends Error { constructor(code, msg, details) { super(msg); this.code = code; - _defineProperty(this, "detailedString", void 0); - this.code = code; - this.name = 'DecryptionError'; + this.name = "DecryptionError"; this.detailedString = detailedStringForDecryptionError(this, details); } - } - exports.DecryptionError = DecryptionError; - function detailedStringForDecryptionError(err, details) { - let result = err.name + '[msg: ' + err.message; - + let result = err.name + "[msg: " + err.message; if (details) { - result += ', ' + Object.keys(details).map(k => k + ': ' + details[k]).join(', '); + result += ", " + Object.keys(details).map(k => k + ": " + details[k]).join(", "); } - - result += ']'; + result += "]"; return result; } -/** - * Exception thrown specifically when we want to warn the user to consider - * the security of their conversation before continuing - * - * @param {string} msg message describing the problem - * @param {Object} devices userId -> {deviceId -> object} - * set of unknown devices per user we're warning about - * @extends Error - */ - - class UnknownDeviceError extends Error { - constructor(msg, devices) { + /** + * Exception thrown specifically when we want to warn the user to consider + * the security of their conversation before continuing + * + * @param msg - message describing the problem + * @param devices - set of unknown devices per user we're warning about + */ + constructor(msg, devices, event) { super(msg); this.devices = devices; + this.event = event; this.name = "UnknownDeviceError"; this.devices = devices; } - } + /** * Registers an encryption/decryption class for a particular algorithm * - * @param {string} algorithm algorithm tag to register for + * @param algorithm - algorithm tag to register for * - * @param {class} encryptor {@link - * module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} - * implementation + * @param encryptor - {@link EncryptionAlgorithm} implementation * - * @param {class} decryptor {@link - * module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} - * implementation + * @param decryptor - {@link DecryptionAlgorithm} implementation */ - - exports.UnknownDeviceError = UnknownDeviceError; - function registerAlgorithm(algorithm, encryptor, decryptor) { ENCRYPTION_CLASSES.set(algorithm, encryptor); DECRYPTION_CLASSES.set(algorithm, decryptor); diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/index.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,13 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); - require("./olm"); - require("./megolm"); - var _base = require("./base"); - Object.keys(_base).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (key in exports && exports[key] === _base[key]) return; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/megolm.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/megolm.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/megolm.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/megolm.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,280 +3,246 @@ Object.defineProperty(exports, "__esModule", { value: true }); +exports.MegolmEncryption = exports.MegolmDecryption = void 0; exports.isRoomSharedHistory = isRoomSharedHistory; - +var _uuid = require("uuid"); var _logger = require("../../logger"); - var olmlib = _interopRequireWildcard(require("../olmlib")); - var _base = require("./base"); - var _OlmDevice = require("../OlmDevice"); - +var _event = require("../../@types/event"); var _OutgoingRoomKeyRequestManager = require("../OutgoingRoomKeyRequestManager"); - +var _utils = require("../../utils"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // determine whether the key can be shared with invitees function isRoomSharedHistory(room) { - const visibilityEvent = room?.currentState?.getStateEvents("m.room.history_visibility", ""); // NOTE: if the room visibility is unset, it would normally default to + const visibilityEvent = room?.currentState?.getStateEvents("m.room.history_visibility", ""); + // NOTE: if the room visibility is unset, it would normally default to // "world_readable". // (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5) // But we will be paranoid here, and treat it as a situation where the room // is not shared-history - const visibility = visibilityEvent?.getContent()?.history_visibility; return ["world_readable", "shared"].includes(visibility); } - /** - * @private - * @constructor - * - * @param {string} sessionId - * @param {boolean} sharedHistory whether the session can be freely shared with - * other group members, according to the room history visibility settings + * Tests whether an encrypted content has a ciphertext. + * Ciphertext can be a string or object depending on the content type {@link IEncryptedContent}. * - * @property {string} sessionId - * @property {Number} useCount number of times this session has been used - * @property {Number} creationTime when the session was created (ms since the epoch) - * - * @property {object} sharedWithDevices - * devices with which we have shared the session key - * userId -> {deviceId -> SharedWithData} + * @param content - Encrypted content + * @returns true: has ciphertext, else false + */ +const hasCiphertext = content => { + return typeof content.ciphertext === "string" ? !!content.ciphertext.length : !!Object.keys(content.ciphertext).length; +}; + +/** The result of parsing the an `m.room_key` or `m.forwarded_room_key` to-device event */ + +/** + * @internal */ class OutboundSessionInfo { + /** number of times this session has been used */ + + /** when the session was created (ms since the epoch) */ + + /** devices with which we have shared the session key `userId -> {deviceId -> SharedWithData}` */ + + /** + * @param sharedHistory - whether the session can be freely shared with + * other group members, according to the room history visibility settings + */ constructor(sessionId, sharedHistory = false) { this.sessionId = sessionId; this.sharedHistory = sharedHistory; - _defineProperty(this, "useCount", 0); - _defineProperty(this, "creationTime", void 0); - - _defineProperty(this, "sharedWithDevices", {}); - - _defineProperty(this, "blockedDevicesNotified", {}); - + _defineProperty(this, "sharedWithDevices", new _utils.MapWithDefault(() => new Map())); + _defineProperty(this, "blockedDevicesNotified", new _utils.MapWithDefault(() => new Map())); this.creationTime = new Date().getTime(); } + /** * Check if it's time to rotate the session - * - * @param {Number} rotationPeriodMsgs - * @param {Number} rotationPeriodMs - * @return {Boolean} */ - - needsRotation(rotationPeriodMsgs, rotationPeriodMs) { const sessionLifetime = new Date().getTime() - this.creationTime; - if (this.useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { _logger.logger.log("Rotating megolm session after " + this.useCount + " messages, " + sessionLifetime + "ms"); - return true; } - return false; } - markSharedWithDevice(userId, deviceId, deviceKey, chainIndex) { - if (!this.sharedWithDevices[userId]) { - this.sharedWithDevices[userId] = {}; - } - - this.sharedWithDevices[userId][deviceId] = { + this.sharedWithDevices.getOrCreate(userId).set(deviceId, { deviceKey, messageIndex: chainIndex - }; + }); } - markNotifiedBlockedDevice(userId, deviceId) { - if (!this.blockedDevicesNotified[userId]) { - this.blockedDevicesNotified[userId] = {}; - } - - this.blockedDevicesNotified[userId][deviceId] = true; + this.blockedDevicesNotified.getOrCreate(userId).set(deviceId, true); } + /** * Determine if this session has been shared with devices which it shouldn't * have been. * - * @param {Object} devicesInRoom userId -> {deviceId -> object} + * @param devicesInRoom - `userId -> {deviceId -> object}` * devices we should shared the session with. * - * @return {Boolean} true if we have shared the session with devices which aren't + * @returns true if we have shared the session with devices which aren't * in devicesInRoom. */ - - sharedWithTooManyDevices(devicesInRoom) { - for (const userId in this.sharedWithDevices) { - if (!this.sharedWithDevices.hasOwnProperty(userId)) { - continue; - } - - if (!devicesInRoom.hasOwnProperty(userId)) { + for (const [userId, devices] of this.sharedWithDevices) { + if (!devicesInRoom.has(userId)) { _logger.logger.log("Starting new megolm session because we shared with " + userId); - return true; } - - for (const deviceId in this.sharedWithDevices[userId]) { - if (!this.sharedWithDevices[userId].hasOwnProperty(deviceId)) { - continue; - } - - if (!devicesInRoom[userId].hasOwnProperty(deviceId)) { + for (const [deviceId] of devices) { + if (!devicesInRoom.get(userId)?.get(deviceId)) { _logger.logger.log("Starting new megolm session because we shared with " + userId + ":" + deviceId); - return true; } } } - return false; } - } + /** * Megolm encryption implementation * - * @constructor - * @extends {module:crypto/algorithms/EncryptionAlgorithm} - * - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/EncryptionAlgorithm} + * @param params - parameters, as per {@link EncryptionAlgorithm} */ - - class MegolmEncryption extends _base.EncryptionAlgorithm { // the most recent attempt to set up a session. This is used to serialise // the session setups, so that we have a race-free view of which session we // are using, and which devices we have shared the keys with. It resolves // with an OutboundSessionInfo (or undefined, for the first message in the // room). + // Map of outbound sessions by sessions ID. Used if we need a particular // session (the session we're currently using to send is always obtained // using setupPromise). + constructor(params) { super(params); - _defineProperty(this, "setupPromise", Promise.resolve(null)); - _defineProperty(this, "outboundSessions", {}); - _defineProperty(this, "sessionRotationPeriodMsgs", void 0); - _defineProperty(this, "sessionRotationPeriodMs", void 0); - _defineProperty(this, "encryptionPreparation", void 0); - + _defineProperty(this, "roomId", void 0); + _defineProperty(this, "prefixedLogger", void 0); + this.roomId = params.roomId; + this.prefixedLogger = _logger.logger.withPrefix(`[${this.roomId} encryption]`); this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100; this.sessionRotationPeriodMs = params.config?.rotation_period_ms ?? 7 * 24 * 3600 * 1000; } + /** - * @private + * @internal * - * @param {module:models/room} room - * @param {Object} devicesInRoom The devices in this room, indexed by user ID - * @param {Object} blocked The devices that are blocked, indexed by user ID - * @param {boolean} [singleOlmCreationPhase] Only perform one round of olm + * @param devicesInRoom - The devices in this room, indexed by user ID + * @param blocked - The devices that are blocked, indexed by user ID + * @param singleOlmCreationPhase - Only perform one round of olm * session creation * - * @return {Promise} Promise which resolves to the + * This method updates the setupPromise field of the class by chaining a new + * call on top of the existing promise, and then catching and discarding any + * errors that might happen while setting up the outbound group session. This + * is done to ensure that `setupPromise` always resolves to `null` or the + * `OutboundSessionInfo`. + * + * Using `>>=` to represent the promise chaining operation, it does the + * following: + * + * ``` + * setupPromise = previousSetupPromise >>= setup >>= discardErrors + * ``` + * + * The initial value for the `setupPromise` is a promise that resolves to + * `null`. The forceDiscardSession() resets setupPromise to this initial + * promise. + * + * @returns Promise which resolves to the * OutboundSessionInfo when setup is complete. */ - - async ensureOutboundSession(room, devicesInRoom, blocked, singleOlmCreationPhase = false) { // takes the previous OutboundSessionInfo, and considers whether to create // a new one. Also shares the key with any (new) devices in the room. // - // Returns the successful session whether keyshare succeeds or not. - // // returns a promise which resolves once the keyshare is successful. const setup = async oldSession => { const sharedHistory = isRoomSharedHistory(room); const session = await this.prepareSession(devicesInRoom, sharedHistory, oldSession); - - try { - await this.shareSession(devicesInRoom, sharedHistory, singleOlmCreationPhase, blocked, session); - } catch (e) { - _logger.logger.error(`Failed to ensure outbound session in ${this.roomId}`, e); - } - + await this.shareSession(devicesInRoom, sharedHistory, singleOlmCreationPhase, blocked, session); return session; - }; // first wait for the previous share to complete - - - const prom = this.setupPromise.then(setup); // Ensure any failures are logged for debugging + }; - prom.catch(e => { - _logger.logger.error(`Failed to setup outbound session in ${this.roomId}`, e); - }); // setupPromise resolves to `session` whether or not the share succeeds + // first wait for the previous share to complete + const fallible = this.setupPromise.then(setup); - this.setupPromise = prom; // but we return a promise which only resolves if the share was successful. + // Ensure any failures are logged for debugging and make sure that the + // promise chain remains unbroken + // + // setupPromise resolves to `null` or the `OutboundSessionInfo` whether + // or not the share succeeds + this.setupPromise = fallible.catch(e => { + this.prefixedLogger.error(`Failed to setup outbound session`, e); + return null; + }); - return prom; + // but we return a promise which only resolves if the share was successful. + return fallible; } - async prepareSession(devicesInRoom, sharedHistory, session) { // history visibility changed if (session && sharedHistory !== session.sharedHistory) { session = null; - } // need to make a brand new session? - + } + // need to make a brand new session? if (session?.needsRotation(this.sessionRotationPeriodMsgs, this.sessionRotationPeriodMs)) { - _logger.logger.log("Starting new megolm session because we need to rotate."); - + this.prefixedLogger.log("Starting new megolm session because we need to rotate."); session = null; - } // determine if we have shared with anyone we shouldn't have - + } + // determine if we have shared with anyone we shouldn't have if (session?.sharedWithTooManyDevices(devicesInRoom)) { session = null; } - if (!session) { - _logger.logger.log(`Starting new megolm session for room ${this.roomId}`); - + this.prefixedLogger.log("Starting new megolm session"); session = await this.prepareNewSession(sharedHistory); - - _logger.logger.log(`Started new megolm session ${session.sessionId} ` + `for room ${this.roomId}`); - + this.prefixedLogger.log(`Started new megolm session ${session.sessionId}`); this.outboundSessions[session.sessionId] = session; } - return session; } - async shareSession(devicesInRoom, sharedHistory, singleOlmCreationPhase, blocked, session) { // now check if we need to share with any devices const shareMap = {}; - - for (const [userId, userDevices] of Object.entries(devicesInRoom)) { - for (const [deviceId, deviceInfo] of Object.entries(userDevices)) { + for (const [userId, userDevices] of devicesInRoom) { + for (const [deviceId, deviceInfo] of userDevices) { const key = deviceInfo.getIdentityKey(); - if (key == this.olmDevice.deviceCurve25519Key) { // don't bother sending to ourself continue; } - - if (!session.sharedWithDevices[userId] || session.sharedWithDevices[userId][deviceId] === undefined) { + if (!session.sharedWithDevices.get(userId)?.get(deviceId)) { shareMap[userId] = shareMap[userId] || []; shareMap[userId].push(deviceInfo); } } } - const key = this.olmDevice.getOutboundGroupSessionKey(session.sessionId); const payload = { type: "m.room_key", @@ -292,26 +258,24 @@ const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(this.olmDevice, this.baseApis, shareMap); await Promise.all([(async () => { // share keys with devices that we already have a session for - _logger.logger.debug(`Sharing keys with existing Olm sessions in ${this.roomId}`, olmSessions); - + const olmSessionList = Array.from(olmSessions.entries()).map(([userId, sessionsByUser]) => Array.from(sessionsByUser.entries()).map(([deviceId, session]) => `${userId}/${deviceId}: ${session.sessionId}`)).flat(1); + this.prefixedLogger.debug("Sharing keys with devices with existing Olm sessions:", olmSessionList); await this.shareKeyWithOlmSessions(session, key, payload, olmSessions); - - _logger.logger.debug(`Shared keys with existing Olm sessions in ${this.roomId}`); + this.prefixedLogger.debug("Shared keys with existing Olm sessions"); })(), (async () => { - _logger.logger.debug(`Sharing keys (start phase 1) with new Olm sessions in ${this.roomId}`, devicesWithoutSession); + const deviceList = Array.from(devicesWithoutSession.entries()).map(([userId, devicesByUser]) => devicesByUser.map(device => `${userId}/${device.deviceId}`)).flat(1); + this.prefixedLogger.debug("Sharing keys (start phase 1) with devices without existing Olm sessions:", deviceList); + const errorDevices = []; - const errorDevices = []; // meanwhile, establish olm sessions for devices that we don't + // meanwhile, establish olm sessions for devices that we don't // already have a session for, and share keys with them. If // we're doing two phases of olm session creation, use a // shorter timeout when fetching one-time keys for the first // phase. - const start = Date.now(); const failedServers = []; await this.shareKeyWithDevices(session, key, payload, devicesWithoutSession, errorDevices, singleOlmCreationPhase ? 10000 : 2000, failedServers); - - _logger.logger.debug(`Shared keys (end phase 1) with new Olm sessions in ${this.roomId}`); - + this.prefixedLogger.debug("Shared keys (end phase 1) with devices without existing Olm sessions"); if (!singleOlmCreationPhase && Date.now() - start < 10000) { // perform the second phase of olm session creation if requested, // and if the first phase didn't take too long @@ -321,24 +285,19 @@ // do this in the background and don't block anything else while we // do this. We only need to retry users from servers that didn't // respond the first time. - const retryDevices = {}; + const retryDevices = new _utils.MapWithDefault(() => []); const failedServerMap = new Set(); - for (const server of failedServers) { failedServerMap.add(server); } - const failedDevices = []; - for (const { userId, deviceInfo } of errorDevices) { const userHS = userId.slice(userId.indexOf(":") + 1); - if (failedServerMap.has(userHS)) { - retryDevices[userId] = retryDevices[userId] || []; - retryDevices[userId].push(deviceInfo); + retryDevices.getOrCreate(userId).push(deviceInfo); } else { // if we aren't going to retry, then handle it // as a failed device @@ -348,55 +307,47 @@ }); } } - - _logger.logger.debug(`Sharing keys (start phase 2) with new Olm sessions in ${this.roomId}`); - - await this.shareKeyWithDevices(session, key, payload, retryDevices, failedDevices, 30000); - - _logger.logger.debug(`Shared keys (end phase 2) with new Olm sessions in ${this.roomId}`); - + const retryDeviceList = Array.from(retryDevices.entries()).map(([userId, devicesByUser]) => devicesByUser.map(device => `${userId}/${device.deviceId}`)).flat(1); + if (retryDeviceList.length > 0) { + this.prefixedLogger.debug("Sharing keys (start phase 2) with devices without existing Olm sessions:", retryDeviceList); + await this.shareKeyWithDevices(session, key, payload, retryDevices, failedDevices, 30000); + this.prefixedLogger.debug("Shared keys (end phase 2) with devices without existing Olm sessions"); + } await this.notifyFailedOlmDevices(session, key, failedDevices); })(); } else { await this.notifyFailedOlmDevices(session, key, errorDevices); } - - _logger.logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this.roomId}`); })(), (async () => { - _logger.logger.debug(`There are ${Object.entries(blocked).length} blocked devices in ${this.roomId}`, Object.entries(blocked)); // also, notify newly blocked devices that they're blocked - + this.prefixedLogger.debug(`There are ${blocked.size} blocked devices:`, Array.from(blocked.entries()).map(([userId, blockedByUser]) => Array.from(blockedByUser.entries()).map(([deviceId, _deviceInfo]) => `${userId}/${deviceId}`)).flat(1)); - _logger.logger.debug(`Notifying newly blocked devices in ${this.roomId}`); - - const blockedMap = {}; + // also, notify newly blocked devices that they're blocked + const blockedMap = new _utils.MapWithDefault(() => new Map()); let blockedCount = 0; - - for (const [userId, userBlockedDevices] of Object.entries(blocked)) { - for (const [deviceId, device] of Object.entries(userBlockedDevices)) { - if (!session.blockedDevicesNotified[userId] || session.blockedDevicesNotified[userId][deviceId] === undefined) { - blockedMap[userId] = blockedMap[userId] || {}; - blockedMap[userId][deviceId] = { + for (const [userId, userBlockedDevices] of blocked) { + for (const [deviceId, device] of userBlockedDevices) { + if (session.blockedDevicesNotified.get(userId)?.get(deviceId) === undefined) { + blockedMap.getOrCreate(userId).set(deviceId, { device - }; + }); blockedCount++; } } } - - await this.notifyBlockedDevices(session, blockedMap); - - _logger.logger.debug(`Notified ${blockedCount} newly blocked devices in ${this.roomId}`, blockedMap); + if (blockedCount) { + this.prefixedLogger.debug(`Notifying ${blockedCount} newly blocked devices:`, Array.from(blockedMap.entries()).map(([userId, blockedByUser]) => Object.entries(blockedByUser).map(([deviceId, _deviceInfo]) => `${userId}/${deviceId}`)).flat(1)); + await this.notifyBlockedDevices(session, blockedMap); + this.prefixedLogger.debug(`Notified ${blockedCount} newly blocked devices`); + } })()]); } + /** - * @private + * @internal * - * @param {boolean} sharedHistory * - * @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session + * @returns session */ - - async prepareNewSession(sharedHistory) { const sessionId = this.olmDevice.createOutboundGroupSession(); const key = this.olmDevice.getOutboundGroupSessionKey(sessionId); @@ -404,114 +355,107 @@ ed25519: this.olmDevice.deviceEd25519Key }, false, { sharedHistory - }); // don't wait for it to complete + }); + // don't wait for it to complete this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key, sessionId); return new OutboundSessionInfo(sessionId, sharedHistory); } + /** * Determines what devices in devicesByUser don't have an olm session as given * in devicemap. * - * @private + * @internal * - * @param {object} devicemap the devices that have olm sessions, as returned by + * @param deviceMap - the devices that have olm sessions, as returned by * olmlib.ensureOlmSessionsForDevices. - * @param {object} devicesByUser a map of user IDs to array of deviceInfo - * @param {array} [noOlmDevices] an array to fill with devices that don't have + * @param devicesByUser - a map of user IDs to array of deviceInfo + * @param noOlmDevices - an array to fill with devices that don't have * olm sessions * - * @return {array} an array of devices that don't have olm sessions. If + * @returns an array of devices that don't have olm sessions. If * noOlmDevices is specified, then noOlmDevices will be returned. */ - - - getDevicesWithoutSessions(devicemap, devicesByUser, noOlmDevices = []) { - for (const [userId, devicesToShareWith] of Object.entries(devicesByUser)) { - const sessionResults = devicemap[userId]; - + getDevicesWithoutSessions(deviceMap, devicesByUser, noOlmDevices = []) { + for (const [userId, devicesToShareWith] of devicesByUser) { + const sessionResults = deviceMap.get(userId); for (const deviceInfo of devicesToShareWith) { const deviceId = deviceInfo.deviceId; - const sessionResult = sessionResults[deviceId]; - - if (!sessionResult.sessionId) { + const sessionResult = sessionResults?.get(deviceId); + if (!sessionResult?.sessionId) { // no session with this device, probably because there // were no one-time keys. + noOlmDevices.push({ userId, deviceInfo }); - delete sessionResults[deviceId]; // ensureOlmSessionsForUsers has already done the logging, - // so just skip it. + sessionResults?.delete(deviceId); + // ensureOlmSessionsForUsers has already done the logging, + // so just skip it. continue; } } } - return noOlmDevices; } + /** * Splits the user device map into multiple chunks to reduce the number of * devices we encrypt to per API call. * - * @private + * @internal * - * @param {object} devicesByUser map from userid to list of devices + * @param devicesByUser - map from userid to list of devices * - * @return {array>} the blocked devices, split into chunks + * @returns the blocked devices, split into chunks */ - - splitDevices(devicesByUser) { - const maxDevicesPerRequest = 20; // use an array where the slices of a content map gets stored + const maxDevicesPerRequest = 20; + // use an array where the slices of a content map gets stored let currentSlice = []; const mapSlices = [currentSlice]; - - for (const [userId, userDevices] of Object.entries(devicesByUser)) { - for (const deviceInfo of Object.values(userDevices)) { + for (const [userId, userDevices] of devicesByUser) { + for (const deviceInfo of userDevices.values()) { currentSlice.push({ userId: userId, deviceInfo: deviceInfo.device }); - } // We do this in the per-user loop as we prefer that all messages to the + } + + // We do this in the per-user loop as we prefer that all messages to the // same user end up in the same API call to make it easier for the // server (e.g. only have to send one EDU if a remote user, etc). This // does mean that if a user has many devices we may go over the desired // limit, but its not a hard limit so that is fine. - - if (currentSlice.length > maxDevicesPerRequest) { // the current slice is filled up. Start inserting into the next slice currentSlice = []; mapSlices.push(currentSlice); } } - if (currentSlice.length === 0) { mapSlices.pop(); } - return mapSlices; } + /** - * @private + * @internal * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {number} chainIndex current chain index + * @param chainIndex - current chain index * - * @param {object} userDeviceMap - * mapping from userId to deviceInfo + * @param userDeviceMap - mapping from userId to deviceInfo * - * @param {object} payload fields to include in the encrypted payload + * @param payload - fields to include in the encrypted payload * - * @return {Promise} Promise which resolves once the key sharing + * @returns Promise which resolves once the key sharing * for the given userDeviceMap is generated and has been sent. */ - - encryptAndSendKeysToDevices(session, chainIndex, devices, payload) { return this.crypto.encryptAndSendToDevices(devices, payload).then(() => { // store that we successfully uploaded the keys of the current slice @@ -519,111 +463,89 @@ session.markSharedWithDevice(device.userId, device.deviceInfo.deviceId, device.deviceInfo.getIdentityKey(), chainIndex); } }).catch(error => { - _logger.logger.error("failed to encryptAndSendToDevices", error); - + this.prefixedLogger.error("failed to encryptAndSendToDevices", error); throw error; }); } + /** - * @private + * @internal * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {array} userDeviceMap list of blocked devices to notify + * @param userDeviceMap - list of blocked devices to notify * - * @param {object} payload fields to include in the notification payload + * @param payload - fields to include in the notification payload * - * @return {Promise} Promise which resolves once the notifications + * @returns Promise which resolves once the notifications * for the given userDeviceMap is generated and has been sent. */ - - async sendBlockedNotificationsToDevices(session, userDeviceMap, payload) { - const contentMap = {}; - + const contentMap = new _utils.MapWithDefault(() => new Map()); for (const val of userDeviceMap) { const userId = val.userId; const blockedInfo = val.deviceInfo; const deviceInfo = blockedInfo.deviceInfo; const deviceId = deviceInfo.deviceId; - const message = Object.assign({}, payload); - message.code = blockedInfo.code; - message.reason = blockedInfo.reason; - + const message = _objectSpread(_objectSpread({}, payload), {}, { + code: blockedInfo.code, + reason: blockedInfo.reason, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() + }); if (message.code === "m.no_olm") { delete message.room_id; delete message.session_id; } - - if (!contentMap[userId]) { - contentMap[userId] = {}; - } - - contentMap[userId][deviceId] = message; + contentMap.getOrCreate(userId).set(deviceId, message); } + await this.baseApis.sendToDevice("m.room_key.withheld", contentMap); - await this.baseApis.sendToDevice("m.room_key.withheld", contentMap); // record the fact that we notified these blocked devices - - for (const userId of Object.keys(contentMap)) { - for (const deviceId of Object.keys(contentMap[userId])) { + // record the fact that we notified these blocked devices + for (const [userId, userDeviceMap] of contentMap) { + for (const deviceId of userDeviceMap.keys()) { session.markNotifiedBlockedDevice(userId, deviceId); } } } + /** * Re-shares a megolm session key with devices if the key has already been * sent to them. * - * @param {string} senderKey The key of the originating device for the session - * @param {string} sessionId ID of the outbound session to share - * @param {string} userId ID of the user who owns the target device - * @param {module:crypto/deviceinfo} device The target device + * @param senderKey - The key of the originating device for the session + * @param sessionId - ID of the outbound session to share + * @param userId - ID of the user who owns the target device + * @param device - The target device */ - - async reshareKeyWithDevice(senderKey, sessionId, userId, device) { const obSessionInfo = this.outboundSessions[sessionId]; - if (!obSessionInfo) { - _logger.logger.debug(`megolm session ${sessionId} not found: not re-sharing keys`); - + this.prefixedLogger.debug(`megolm session ${senderKey}|${sessionId} not found: not re-sharing keys`); return; - } // The chain index of the key we previously sent this device - - - if (obSessionInfo.sharedWithDevices[userId] === undefined) { - _logger.logger.debug(`megolm session ${sessionId} never shared with user ${userId}`); + } + // The chain index of the key we previously sent this device + if (!obSessionInfo.sharedWithDevices.has(userId)) { + this.prefixedLogger.debug(`megolm session ${senderKey}|${sessionId} never shared with user ${userId}`); return; } - - const sessionSharedData = obSessionInfo.sharedWithDevices[userId][device.deviceId]; - + const sessionSharedData = obSessionInfo.sharedWithDevices.get(userId)?.get(device.deviceId); if (sessionSharedData === undefined) { - _logger.logger.debug("megolm session ID " + sessionId + " never shared with device " + userId + ":" + device.deviceId); - + this.prefixedLogger.debug(`megolm session ${senderKey}|${sessionId} never shared with device ${userId}:${device.deviceId}`); return; } - if (sessionSharedData.deviceKey !== device.getIdentityKey()) { - _logger.logger.warn(`Session has been shared with device ${device.deviceId} but with identity ` + `key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`); - + this.prefixedLogger.warn(`Megolm session ${senderKey}|${sessionId} has been shared with device ${device.deviceId} but ` + `with identity key ${sessionSharedData.deviceKey}. Key is now ${device.getIdentityKey()}!`); return; - } // get the key from the inbound session: the outbound one will already - // have been ratcheted to the next chain index. - + } + // get the key from the inbound session: the outbound one will already + // have been ratcheted to the next chain index. const key = await this.olmDevice.getInboundGroupSessionKey(this.roomId, senderKey, sessionId, sessionSharedData.messageIndex); - if (!key) { - _logger.logger.warn(`No inbound session key found for megolm ${sessionId}: not re-sharing keys`); - + this.prefixedLogger.warn(`No inbound session key found for megolm session ${senderKey}|${sessionId}: not re-sharing keys`); return; } - - await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, { - [userId]: [device] - }); + await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[userId, [device]]])); const payload = { type: "m.forwarded_room_key", content: { @@ -641,94 +563,67 @@ const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, - ciphertext: {} + ciphertext: {}, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() }; await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this.userId, this.deviceId, this.olmDevice, userId, device, payload); - await this.baseApis.sendToDevice("m.room.encrypted", { - [userId]: { - [device.deviceId]: encryptedContent - } - }); - - _logger.logger.debug(`Re-shared key for megolm session ${sessionId} with ${userId}:${device.deviceId}`); + await this.baseApis.sendToDevice("m.room.encrypted", new Map([[userId, new Map([[device.deviceId, encryptedContent]])]])); + this.prefixedLogger.debug(`Re-shared key for megolm session ${senderKey}|${sessionId} with ${userId}:${device.deviceId}`); } + /** - * @private + * @internal * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {object} key the session key as returned by + * @param key - the session key as returned by * OlmDevice.getOutboundGroupSessionKey * - * @param {object} payload the base to-device message payload for sharing keys + * @param payload - the base to-device message payload for sharing keys * - * @param {object} devicesByUser - * map from userid to list of devices + * @param devicesByUser - map from userid to list of devices * - * @param {array} errorDevices - * array that will be populated with the devices that we can't get an + * @param errorDevices - array that will be populated with the devices that we can't get an * olm session for * - * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * @param otkTimeout - The timeout in milliseconds when requesting * one-time keys for establishing new olm sessions. * - * @param {Array} [failedServers] An array to fill with remote servers that + * @param failedServers - An array to fill with remote servers that * failed to respond to one-time-key requests. */ - - async shareKeyWithDevices(session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers) { - _logger.logger.debug(`Ensuring Olm sessions for devices in ${this.roomId}`); - - const devicemap = await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, false, otkTimeout, failedServers, _logger.logger.withPrefix?.(`[${this.roomId}]`)); - - _logger.logger.debug(`Ensured Olm sessions for devices in ${this.roomId}`); - + const devicemap = await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, false, otkTimeout, failedServers, this.prefixedLogger); this.getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices); - - _logger.logger.debug(`Sharing keys with newly created Olm sessions in ${this.roomId}`); - await this.shareKeyWithOlmSessions(session, key, payload, devicemap); - - _logger.logger.debug(`Shared keys with newly created Olm sessions in ${this.roomId}`); } - - async shareKeyWithOlmSessions(session, key, payload, devicemap) { - const userDeviceMaps = this.splitDevices(devicemap); - + async shareKeyWithOlmSessions(session, key, payload, deviceMap) { + const userDeviceMaps = this.splitDevices(deviceMap); for (let i = 0; i < userDeviceMaps.length; i++) { - const taskDetail = `megolm keys for ${session.sessionId} ` + `in ${this.roomId} (slice ${i + 1}/${userDeviceMaps.length})`; - + const taskDetail = `megolm keys for ${session.sessionId} (slice ${i + 1}/${userDeviceMaps.length})`; try { - _logger.logger.debug(`Sharing ${taskDetail}`, userDeviceMaps[i].map(d => `${d.userId}/${d.deviceInfo.deviceId}`)); - + this.prefixedLogger.debug(`Sharing ${taskDetail}`, userDeviceMaps[i].map(d => `${d.userId}/${d.deviceInfo.deviceId}`)); await this.encryptAndSendKeysToDevices(session, key.chain_index, userDeviceMaps[i], payload); - - _logger.logger.debug(`Shared ${taskDetail}`); + this.prefixedLogger.debug(`Shared ${taskDetail}`); } catch (e) { - _logger.logger.error(`Failed to share ${taskDetail}`); - + this.prefixedLogger.error(`Failed to share ${taskDetail}`); throw e; } } } + /** * Notify devices that we weren't able to create olm sessions. * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {object} key * - * @param {Array} failedDevices the devices that we were unable to + * @param failedDevices - the devices that we were unable to * create olm sessions for, as returned by shareKeyWithDevices */ - - async notifyFailedOlmDevices(session, key, failedDevices) { - _logger.logger.debug(`Notifying ${failedDevices.length} devices we failed to ` + `create Olm sessions in ${this.roomId}`); // mark the devices that failed as "handled" because we don't want to try - // to claim a one-time-key for dead devices on every message. - + this.prefixedLogger.debug(`Notifying ${failedDevices.length} devices we failed to create Olm sessions`); + // mark the devices that failed as "handled" because we don't want to try + // to claim a one-time-key for dead devices on every message. for (const { userId, deviceInfo @@ -736,45 +631,36 @@ const deviceId = deviceInfo.deviceId; session.markSharedWithDevice(userId, deviceId, deviceInfo.getIdentityKey(), key.chain_index); } - const unnotifiedFailedDevices = await this.olmDevice.filterOutNotifiedErrorDevices(failedDevices); - - _logger.logger.debug(`Need to notify ${unnotifiedFailedDevices.length} failed devices ` + `which haven't been notified before in ${this.roomId}`); - - const blockedMap = {}; - + this.prefixedLogger.debug(`Need to notify ${unnotifiedFailedDevices.length} failed devices which haven't been notified before`); + const blockedMap = new _utils.MapWithDefault(() => new Map()); for (const { userId, deviceInfo } of unnotifiedFailedDevices) { - blockedMap[userId] = blockedMap[userId] || {}; // we use a similar format to what + // we use a similar format to what // olmlib.ensureOlmSessionsForDevices returns, so that // we can use the same function to split - - blockedMap[userId][deviceInfo.deviceId] = { + blockedMap.getOrCreate(userId).set(deviceInfo.deviceId, { device: { code: "m.no_olm", reason: _OlmDevice.WITHHELD_MESSAGES["m.no_olm"], deviceInfo } - }; - } // send the notifications - + }); + } + // send the notifications await this.notifyBlockedDevices(session, blockedMap); - - _logger.logger.debug(`Notified ${unnotifiedFailedDevices.length} devices we failed to ` + `create Olm sessions in ${this.roomId}`); + this.prefixedLogger.debug(`Notified ${unnotifiedFailedDevices.length} devices we failed to create Olm sessions`); } + /** * Notify blocked devices that they have been blocked. * - * @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session * - * @param {object} devicesByUser - * map from userid to device ID to blocked data + * @param devicesByUser - map from userid to device ID to blocked data */ - - async notifyBlockedDevices(session, devicesByUser) { const payload = { room_id: this.roomId, @@ -783,101 +669,102 @@ sender_key: this.olmDevice.deviceCurve25519Key }; const userDeviceMaps = this.splitDevices(devicesByUser); - for (let i = 0; i < userDeviceMaps.length; i++) { try { await this.sendBlockedNotificationsToDevices(session, userDeviceMaps[i], payload); - - _logger.logger.log(`Completed blacklist notification for ${session.sessionId} ` + `in ${this.roomId} (slice ${i + 1}/${userDeviceMaps.length})`); + this.prefixedLogger.log(`Completed blacklist notification for ${session.sessionId} ` + `(slice ${i + 1}/${userDeviceMaps.length})`); } catch (e) { - _logger.logger.log(`blacklist notification for ${session.sessionId} in ` + `${this.roomId} (slice ${i + 1}/${userDeviceMaps.length}) failed`); - + this.prefixedLogger.log(`blacklist notification for ${session.sessionId} ` + `(slice ${i + 1}/${userDeviceMaps.length}) failed`); throw e; } } } + /** * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. * - * @param {module:models/room} room the room the event is in + * @param room - the room the event is in + * @returns A function that, when called, will stop the preparation */ - - prepareToEncrypt(room) { + if (room.roomId !== this.roomId) { + throw new Error("MegolmEncryption.prepareToEncrypt called on unexpected room"); + } if (this.encryptionPreparation != null) { // We're already preparing something, so don't do anything else. - // FIXME: check if we need to restart - // (https://github.com/matrix-org/matrix-js-sdk/issues/1255) const elapsedTime = Date.now() - this.encryptionPreparation.startTime; - - _logger.logger.debug(`Already started preparing to encrypt for ${this.roomId} ` + `${elapsedTime} ms ago, skipping`); - - return; + this.prefixedLogger.debug(`Already started preparing to encrypt for this room ${elapsedTime}ms ago, skipping`); + return this.encryptionPreparation.cancel; } - - _logger.logger.debug(`Preparing to encrypt events for ${this.roomId}`); - + this.prefixedLogger.debug("Preparing to encrypt events"); + let cancelled = false; + const isCancelled = () => cancelled; this.encryptionPreparation = { startTime: Date.now(), promise: (async () => { try { - _logger.logger.debug(`Getting devices in ${this.roomId}`); - - const [devicesInRoom, blocked] = await this.getDevicesInRoom(room); - - if (this.crypto.getGlobalErrorOnUnknownDevices()) { + // Attempt to enumerate the devices in room, and gracefully + // handle cancellation if it occurs. + const getDevicesResult = await this.getDevicesInRoom(room, false, isCancelled); + if (getDevicesResult === null) return; + const [devicesInRoom, blocked] = getDevicesResult; + if (this.crypto.globalErrorOnUnknownDevices) { // Drop unknown devices for now. When the message gets sent, we'll // throw an error, but we'll still be prepared to send to the known // devices. this.removeUnknownDevices(devicesInRoom); } - - _logger.logger.debug(`Ensuring outbound session in ${this.roomId}`); - + this.prefixedLogger.debug("Ensuring outbound megolm session"); await this.ensureOutboundSession(room, devicesInRoom, blocked, true); - - _logger.logger.debug(`Ready to encrypt events for ${this.roomId}`); + this.prefixedLogger.debug("Ready to encrypt events"); } catch (e) { - _logger.logger.error(`Failed to prepare to encrypt events for ${this.roomId}`, e); + this.prefixedLogger.error("Failed to prepare to encrypt events", e); } finally { delete this.encryptionPreparation; } - })() + })(), + cancel: () => { + // The caller has indicated that the process should be cancelled, + // so tell the promise that we'd like to halt, and reset the preparation state. + cancelled = true; + delete this.encryptionPreparation; + } }; + return this.encryptionPreparation.cancel; } + /** - * @inheritdoc + * @param content - plaintext event content * - * @param {module:models/room} room - * @param {string} eventType - * @param {object} content plaintext event content - * - * @return {Promise} Promise which resolves to the new event body + * @returns Promise which resolves to the new event body */ - - async encryptMessage(room, eventType, content) { - _logger.logger.log(`Starting to encrypt event for ${this.roomId}`); - + this.prefixedLogger.log("Starting to encrypt event"); if (this.encryptionPreparation != null) { // If we started sending keys, wait for it to be done. // FIXME: check if we need to cancel // (https://github.com/matrix-org/matrix-js-sdk/issues/1255) try { await this.encryptionPreparation.promise; - } catch (e) {// ignore any errors -- if the preparation failed, we'll just + } catch (e) { + // ignore any errors -- if the preparation failed, we'll just // restart everything here } } - const [devicesInRoom, blocked] = await this.getDevicesInRoom(room); // check if any of these devices are not yet known to the user. - // if so, warn the user so they can verify or ignore. + /** + * When using in-room messages and the room has encryption enabled, + * clients should ensure that encryption does not hinder the verification. + */ + const forceDistributeToUnverified = this.isVerificationEvent(eventType, content); + const [devicesInRoom, blocked] = await this.getDevicesInRoom(room, forceDistributeToUnverified); - if (this.crypto.getGlobalErrorOnUnknownDevices()) { + // check if any of these devices are not yet known to the user. + // if so, warn the user so they can verify or ignore. + if (this.crypto.globalErrorOnUnknownDevices) { this.checkForUnknownDevices(devicesInRoom); } - const session = await this.ensureOutboundSession(room, devicesInRoom, blocked); const payloadJson = { room_id: this.roomId, @@ -899,183 +786,192 @@ session.useCount++; return encryptedContent; } + isVerificationEvent(eventType, content) { + switch (eventType) { + case _event.EventType.KeyVerificationCancel: + case _event.EventType.KeyVerificationDone: + case _event.EventType.KeyVerificationMac: + case _event.EventType.KeyVerificationStart: + case _event.EventType.KeyVerificationKey: + case _event.EventType.KeyVerificationReady: + case _event.EventType.KeyVerificationAccept: + { + return true; + } + case _event.EventType.RoomMessage: + { + return content["msgtype"] === _event.MsgType.KeyVerificationRequest; + } + default: + { + return false; + } + } + } + /** * Forces the current outbound group session to be discarded such * that another one will be created next time an event is sent. * * This should not normally be necessary. */ - - forceDiscardSession() { this.setupPromise = this.setupPromise.then(() => null); } + /** * Checks the devices we're about to send to and see if any are entirely * unknown to the user. If so, warn the user, and mark them as known to * give the user a chance to go verify them before re-sending this message. * - * @param {Object} devicesInRoom userId -> {deviceId -> object} + * @param devicesInRoom - `userId -> {deviceId -> object}` * devices we should shared the session with. */ - - checkForUnknownDevices(devicesInRoom) { - const unknownDevices = {}; - Object.keys(devicesInRoom).forEach(userId => { - Object.keys(devicesInRoom[userId]).forEach(deviceId => { - const device = devicesInRoom[userId][deviceId]; - + const unknownDevices = new _utils.MapWithDefault(() => new Map()); + for (const [userId, userDevices] of devicesInRoom) { + for (const [deviceId, device] of userDevices) { if (device.isUnverified() && !device.isKnown()) { - if (!unknownDevices[userId]) { - unknownDevices[userId] = {}; - } - - unknownDevices[userId][deviceId] = device; + unknownDevices.getOrCreate(userId).set(deviceId, device); } - }); - }); - - if (Object.keys(unknownDevices).length) { + } + } + if (unknownDevices.size) { // it'd be kind to pass unknownDevices up to the user in this error throw new _base.UnknownDeviceError("This room contains unknown devices which have not been verified. " + "We strongly recommend you verify them before continuing.", unknownDevices); } } + /** * Remove unknown devices from a set of devices. The devicesInRoom parameter * will be modified. * - * @param {Object} devicesInRoom userId -> {deviceId -> object} + * @param devicesInRoom - `userId -> {deviceId -> object}` * devices we should shared the session with. */ - - removeUnknownDevices(devicesInRoom) { - for (const [userId, userDevices] of Object.entries(devicesInRoom)) { - for (const [deviceId, device] of Object.entries(userDevices)) { + for (const [userId, userDevices] of devicesInRoom) { + for (const [deviceId, device] of userDevices) { if (device.isUnverified() && !device.isKnown()) { - delete userDevices[deviceId]; + userDevices.delete(deviceId); } } - - if (Object.keys(userDevices).length === 0) { - delete devicesInRoom[userId]; + if (userDevices.size === 0) { + devicesInRoom.delete(userId); } } } + /** * Get the list of unblocked devices for all users in the room * - * @param {module:models/room} room + * @param forceDistributeToUnverified - if set to true will include the unverified devices + * even if setting is set to block them (useful for verification) + * @param isCancelled - will cause the procedure to abort early if and when it starts + * returning `true`. If omitted, cancellation won't happen. * - * @return {Promise} Promise which resolves to an array whose - * first element is a map from userId to deviceId to deviceInfo indicating + * @returns Promise which resolves to `null`, or an array whose + * first element is a {@link DeviceInfoMap} indicating * the devices that messages should be encrypted to, and whose second * element is a map from userId to deviceId to data indicating the devices - * that are in the room but that have been blocked + * that are in the room but that have been blocked. + * If `isCancelled` is provided and returns `true` while processing, `null` + * will be returned. + * If `isCancelled` is not provided, the Promise will never resolve to `null`. */ - - async getDevicesInRoom(room) { + async getDevicesInRoom(room, forceDistributeToUnverified = false, isCancelled) { const members = await room.getEncryptionTargetMembers(); + this.prefixedLogger.debug(`Encrypting for users (shouldEncryptForInvitedMembers: ${room.shouldEncryptForInvitedMembers()}):`, members.map(u => `${u.userId} (${u.membership})`)); const roomMembers = members.map(function (u) { return u.userId; - }); // The global value is treated as a default for when rooms don't specify a value. + }); - let isBlacklisting = this.crypto.getGlobalBlacklistUnverifiedDevices(); + // The global value is treated as a default for when rooms don't specify a value. + let isBlacklisting = this.crypto.globalBlacklistUnverifiedDevices; + const isRoomBlacklisting = room.getBlacklistUnverifiedDevices(); + if (typeof isRoomBlacklisting === "boolean") { + isBlacklisting = isRoomBlacklisting; + } - if (typeof room.getBlacklistUnverifiedDevices() === 'boolean') { - isBlacklisting = room.getBlacklistUnverifiedDevices(); - } // We are happy to use a cached version here: we assume that if we already + // We are happy to use a cached version here: we assume that if we already // have a list of the user's devices, then we already share an e2e room // with them, which means that they will have announced any new devices via // device_lists in their /sync response. This cache should then be maintained // using all the device_lists changes and left fields. // See https://github.com/vector-im/element-web/issues/2305 for details. - - const devices = await this.crypto.downloadKeys(roomMembers, false); - const blocked = {}; // remove any blocked devices - - for (const userId in devices) { - if (!devices.hasOwnProperty(userId)) { - continue; - } - - const userDevices = devices[userId]; - - for (const deviceId in userDevices) { - if (!userDevices.hasOwnProperty(deviceId)) { - continue; - } - + if (isCancelled?.() === true) { + return null; + } + const blocked = new _utils.MapWithDefault(() => new Map()); + // remove any blocked devices + for (const [userId, userDevices] of devices) { + for (const [deviceId, userDevice] of userDevices) { + // Yield prior to checking each device so that we don't block + // updating/rendering for too long. + // See https://github.com/vector-im/element-web/issues/21612 + if (isCancelled !== undefined) await (0, _utils.immediate)(); + if (isCancelled?.() === true) return null; const deviceTrust = this.crypto.checkDeviceTrust(userId, deviceId); - - if (userDevices[deviceId].isBlocked() || !deviceTrust.isVerified() && isBlacklisting) { - if (!blocked[userId]) { - blocked[userId] = {}; - } - - const isBlocked = userDevices[deviceId].isBlocked(); - blocked[userId][deviceId] = { + if (userDevice.isBlocked() || !deviceTrust.isVerified() && isBlacklisting && !forceDistributeToUnverified) { + const blockedDevices = blocked.getOrCreate(userId); + const isBlocked = userDevice.isBlocked(); + blockedDevices.set(deviceId, { code: isBlocked ? "m.blacklisted" : "m.unverified", reason: _OlmDevice.WITHHELD_MESSAGES[isBlocked ? "m.blacklisted" : "m.unverified"], - deviceInfo: userDevices[deviceId] - }; - delete userDevices[deviceId]; + deviceInfo: userDevice + }); + userDevices.delete(deviceId); } } } - return [devices, blocked]; } - } + /** * Megolm decryption implementation * - * @constructor - * @extends {module:crypto/algorithms/DecryptionAlgorithm} - * - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/DecryptionAlgorithm} + * @param params - parameters, as per {@link DecryptionAlgorithm} */ - - +exports.MegolmEncryption = MegolmEncryption; class MegolmDecryption extends _base.DecryptionAlgorithm { - constructor(...args) { - super(...args); + // events which we couldn't decrypt due to unknown sessions / + // indexes, or which we could only decrypt with untrusted keys: + // map from senderKey|sessionId to Set of MatrixEvents - _defineProperty(this, "pendingEvents", new Map()); + // this gets stubbed out by the unit tests. + constructor(params) { + super(params); + _defineProperty(this, "pendingEvents", new Map()); _defineProperty(this, "olmlib", olmlib); + _defineProperty(this, "roomId", void 0); + _defineProperty(this, "prefixedLogger", void 0); + this.roomId = params.roomId; + this.prefixedLogger = _logger.logger.withPrefix(`[${this.roomId} decryption]`); } /** - * @inheritdoc - * - * @param {MatrixEvent} event - * * returns a promise which resolves to a - * {@link module:crypto~EventDecryptionResult} once we have finished + * {@link EventDecryptionResult} once we have finished * decrypting, or rejects with an `algorithms.DecryptionError` if there is a * problem decrypting the event. */ async decryptEvent(event) { const content = event.getWireContent(); - if (!content.sender_key || !content.session_id || !content.ciphertext) { throw new _base.DecryptionError("MEGOLM_MISSING_FIELDS", "Missing fields in input"); - } // we add the event to the pending list *before* we start decryption. + } + + // we add the event to the pending list *before* we start decryption. // // then, if the key turns up while decryption is in progress (and // decryption fails), we will schedule a retry. // (fixes https://github.com/vector-im/element-web/issues/5001) - - this.addEventToPendingList(event); let res; - try { res = await this.olmDevice.decryptGroupMessage(event.getRoomId(), content.sender_key, content.session_id, content.ciphertext, event.getId(), event.getTs()); } catch (e) { @@ -1083,66 +979,60 @@ // re-throw decryption errors as-is throw e; } - let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR"; - - if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { + if (e?.message === "OLM.UNKNOWN_MESSAGE_INDEX") { this.requestKeysForEvent(event); - errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX'; + errorCode = "OLM_UNKNOWN_MESSAGE_INDEX"; } - - throw new _base.DecryptionError(errorCode, e ? e.toString() : "Unknown Error: Error is undefined", { - session: content.sender_key + '|' + content.session_id + throw new _base.DecryptionError(errorCode, e instanceof Error ? e.message : "Unknown Error: Error is undefined", { + session: content.sender_key + "|" + content.session_id }); } - if (res === null) { // We've got a message for a session we don't have. // try and get the missing key from the backup first - this.crypto.backupManager.queryKeyBackupRateLimited(event.getRoomId(), content.session_id).catch(() => {}); // (XXX: We might actually have received this key since we started + this.crypto.backupManager.queryKeyBackupRateLimited(event.getRoomId(), content.session_id).catch(() => {}); + + // (XXX: We might actually have received this key since we started // decrypting, in which case we'll have scheduled a retry, and this // request will be redundant. We could probably check to see if the // event is still in the pending list; if not, a retry will have been // scheduled, so we needn't send out the request here.) + this.requestKeysForEvent(event); - this.requestKeysForEvent(event); // See if there was a problem with the olm session at the time the + // See if there was a problem with the olm session at the time the // event was sent. Use a fuzz factor of 2 minutes. - const problem = await this.olmDevice.sessionMayHaveProblems(content.sender_key, event.getTs() - 120000); - if (problem) { + this.prefixedLogger.info(`When handling UISI from ${event.getSender()} (sender key ${content.sender_key}): ` + `recent session problem with that sender:`, problem); let problemDescription = PROBLEM_DESCRIPTIONS[problem.type] || PROBLEM_DESCRIPTIONS.unknown; - if (problem.fixed) { problemDescription += " Trying to create a new secure channel and re-requesting the keys."; } - throw new _base.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", problemDescription, { - session: content.sender_key + '|' + content.session_id + session: content.sender_key + "|" + content.session_id }); } - throw new _base.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", "The sender's device has not sent us the keys for this message.", { - session: content.sender_key + '|' + content.session_id + session: content.sender_key + "|" + content.session_id }); - } // Success. We can remove the event from the pending list, if + } + + // Success. We can remove the event from the pending list, if // that hasn't already happened. However, if the event was // decrypted with an untrusted key, leave it on the pending // list so it will be retried if we find a trusted key later. - - if (!res.untrusted) { this.removeEventFromPendingList(event); } + const payload = JSON.parse(res.result); - const payload = JSON.parse(res.result); // belt-and-braces check that the room id matches that indicated by the HS + // belt-and-braces check that the room id matches that indicated by the HS // (this is somewhat redundant, since the megolm session is scoped to the // room, so neither the sender nor a MITM can lie about the room_id). - if (payload.room_id !== event.getRoomId()) { throw new _base.DecryptionError("MEGOLM_BAD_ROOM", "Message intended for room " + payload.room_id); } - return { clearEvent: payload, senderCurve25519Key: res.senderKey, @@ -1151,7 +1041,6 @@ untrusted: res.untrusted }; } - requestKeysForEvent(event) { const wireContent = event.getWireContent(); const recipients = event.getKeyRequestRecipients(this.userId); @@ -1162,308 +1051,441 @@ session_id: wireContent.session_id }, recipients); } + /** * Add an event to the list of those awaiting their session keys. * - * @private + * @internal * - * @param {module:models/event.MatrixEvent} event */ - - addEventToPendingList(event) { const content = event.getWireContent(); const senderKey = content.sender_key; const sessionId = content.session_id; - if (!this.pendingEvents.has(senderKey)) { this.pendingEvents.set(senderKey, new Map()); } - const senderPendingEvents = this.pendingEvents.get(senderKey); - if (!senderPendingEvents.has(sessionId)) { senderPendingEvents.set(sessionId, new Set()); } - senderPendingEvents.get(sessionId)?.add(event); } + /** * Remove an event from the list of those awaiting their session keys. * - * @private + * @internal * - * @param {module:models/event.MatrixEvent} event */ - - removeEventFromPendingList(event) { const content = event.getWireContent(); const senderKey = content.sender_key; const sessionId = content.session_id; const senderPendingEvents = this.pendingEvents.get(senderKey); const pendingEvents = senderPendingEvents?.get(sessionId); - if (!pendingEvents) { return; } - pendingEvents.delete(event); - if (pendingEvents.size === 0) { senderPendingEvents.delete(sessionId); } - if (senderPendingEvents.size === 0) { this.pendingEvents.delete(senderKey); } } + /** - * @inheritdoc + * Parse a RoomKey out of an `m.room_key` event. + * + * @param event - the event containing the room key. + * + * @returns The `RoomKey` if it could be successfully parsed out of the + * event. + * + * @internal * - * @param {module:models/event.MatrixEvent} event key event */ - - - async onRoomKeyEvent(event) { + roomKeyFromEvent(event) { + const senderKey = event.getSenderKey(); const content = event.getContent(); - let senderKey = event.getSenderKey(); - let forwardingKeyChain = []; - let exportFormat = false; - let keysClaimed; const extraSessionData = {}; - if (!content.room_id || !content.session_key || !content.session_id || !content.algorithm) { - _logger.logger.error("key event is missing fields"); - + this.prefixedLogger.error("key event is missing fields"); return; } - if (!olmlib.isOlmEncrypted(event)) { - _logger.logger.error("key event not properly encrypted"); - + this.prefixedLogger.error("key event not properly encrypted"); return; } - if (content["org.matrix.msc3061.shared_history"]) { extraSessionData.sharedHistory = true; } + const roomKey = { + senderKey: senderKey, + sessionId: content.session_id, + sessionKey: content.session_key, + extraSessionData, + exportFormat: false, + roomId: content.room_id, + algorithm: content.algorithm, + forwardingKeyChain: [], + keysClaimed: event.getKeysClaimed() + }; + return roomKey; + } - if (event.getType() == "m.forwarded_room_key") { - const deviceInfo = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey); - const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, senderKey); - - if (senderKeyUser !== event.getSender()) { - _logger.logger.error("sending device does not belong to the user it claims to be from"); - - return; - } - - const outgoingRequests = deviceInfo ? await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(event.getSender(), deviceInfo.deviceId, [_OutgoingRoomKeyRequestManager.RoomKeyRequestState.Sent]) : []; - const weRequested = outgoingRequests.some(req => req.requestBody.room_id === content.room_id && req.requestBody.session_id === content.session_id); - const room = this.baseApis.getRoom(content.room_id); - const memberEvent = room?.getMember(this.userId)?.events.member; - const fromInviter = memberEvent?.getSender() === event.getSender() || memberEvent?.getUnsigned()?.prev_sender === event.getSender() && memberEvent?.getPrevContent()?.membership === "invite"; - const fromUs = event.getSender() === this.baseApis.getUserId(); - - if (!weRequested) { - // If someone sends us an unsolicited key and it's not - // shared history, ignore it - if (!extraSessionData.sharedHistory) { - _logger.logger.log("forwarded key not shared history - ignoring"); - - return; - } // If someone sends us an unsolicited key for a room - // we're already in, and they're not one of our other - // devices or the one who invited us, ignore it - - - if (room && !fromInviter && !fromUs) { - _logger.logger.log("forwarded key not from inviter or from us - ignoring"); - - return; - } - } - - exportFormat = true; - forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ? content.forwarding_curve25519_key_chain : []; // copy content before we modify it - - forwardingKeyChain = forwardingKeyChain.slice(); - forwardingKeyChain.push(senderKey); - - if (!content.sender_key) { - _logger.logger.error("forwarded_room_key event is missing sender_key field"); - - return; - } - - const ed25519Key = content.sender_claimed_ed25519_key; - - if (!ed25519Key) { - _logger.logger.error(`forwarded_room_key_event is missing sender_claimed_ed25519_key field`); - - return; - } + /** + * Parse a RoomKey out of an `m.forwarded_room_key` event. + * + * @param event - the event containing the forwarded room key. + * + * @returns The `RoomKey` if it could be successfully parsed out of the + * event. + * + * @internal + * + */ + forwardedRoomKeyFromEvent(event) { + // the properties in m.forwarded_room_key are a superset of those in m.room_key, so + // start by parsing the m.room_key fields. + const roomKey = this.roomKeyFromEvent(event); + if (!roomKey) { + return; + } + const senderKey = event.getSenderKey(); + const content = event.getContent(); + const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, senderKey); - keysClaimed = { - ed25519: ed25519Key - }; // If this is a key for a room we're not in, don't load it - // yet, just park it in case *this sender* invites us to - // that room later - - if (!room) { - const parkedData = { - senderId: event.getSender(), - senderKey: content.sender_key, - sessionId: content.session_id, - sessionKey: content.session_key, - keysClaimed, - forwardingCurve25519KeyChain: forwardingKeyChain - }; - await this.crypto.cryptoStore.doTxn('readwrite', ['parked_shared_history'], txn => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn), _logger.logger.withPrefix("[addParkedSharedHistory]")); - return; - } + // We received this to-device event from event.getSenderKey(), but the original + // creator of the room key is claimed in the content. + const claimedCurve25519Key = content.sender_key; + const claimedEd25519Key = content.sender_claimed_ed25519_key; + let forwardingKeyChain = Array.isArray(content.forwarding_curve25519_key_chain) ? content.forwarding_curve25519_key_chain : []; + + // copy content before we modify it + forwardingKeyChain = forwardingKeyChain.slice(); + forwardingKeyChain.push(senderKey); + + // Check if we have all the fields we need. + if (senderKeyUser !== event.getSender()) { + this.prefixedLogger.error("sending device does not belong to the user it claims to be from"); + return; + } + if (!claimedCurve25519Key) { + this.prefixedLogger.error("forwarded_room_key event is missing sender_key field"); + return; + } + if (!claimedEd25519Key) { + this.prefixedLogger.error(`forwarded_room_key_event is missing sender_claimed_ed25519_key field`); + return; + } + const keysClaimed = { + ed25519: claimedEd25519Key + }; - const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey); - const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice); + // FIXME: We're reusing the same field to track both: + // + // 1. The Olm identity we've received this room key from. + // 2. The Olm identity deduced (in the trusted case) or claiming (in the + // untrusted case) to be the original creator of this room key. + // + // We now overwrite the value tracking usage 1 with the value tracking usage 2. + roomKey.senderKey = claimedCurve25519Key; + // Replace our keysClaimed as well. + roomKey.keysClaimed = keysClaimed; + roomKey.exportFormat = true; + roomKey.forwardingKeyChain = forwardingKeyChain; + // forwarded keys are always untrusted + roomKey.extraSessionData.untrusted = true; + return roomKey; + } - if (fromUs && !deviceTrust.isVerified()) { - return; - } // forwarded keys are always untrusted + /** + * Determine if we should accept the forwarded room key that was found in the given + * event. + * + * @param event - An `m.forwarded_room_key` event. + * @param roomKey - The room key that was found in the event. + * + * @returns promise that will resolve to a boolean telling us if it's ok to + * accept the given forwarded room key. + * + * @internal + * + */ + async shouldAcceptForwardedKey(event, roomKey) { + const senderKey = event.getSenderKey(); + const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey) ?? undefined; + const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice); + // Using the plaintext sender here is fine since we checked that the + // sender matches to the user id in the device keys when this event was + // originally decrypted. This can obviously only happen if the device + // keys have been downloaded, but if they haven't the + // `deviceTrust.isVerified()` flag would be false as well. + // + // It would still be far nicer if the `sendingDevice` had a user ID + // attached to it that went through signature checks. + const fromUs = event.getSender() === this.baseApis.getUserId(); + const keyFromOurVerifiedDevice = deviceTrust.isVerified() && fromUs; + const weRequested = await this.wasRoomKeyRequested(event, roomKey); + const fromInviter = this.wasRoomKeyForwardedByInviter(event, roomKey); + const sharedAsHistory = this.wasRoomKeyForwardedAsHistory(roomKey); + return weRequested && keyFromOurVerifiedDevice || fromInviter && sharedAsHistory; + } - extraSessionData.untrusted = true; // replace the sender key with the sender key of the session - // creator for storage + /** + * Did we ever request the given room key from the event sender and its + * accompanying device. + * + * @param event - An `m.forwarded_room_key` event. + * @param roomKey - The room key that was found in the event. + * + * @internal + * + */ + async wasRoomKeyRequested(event, roomKey) { + // We send the `m.room_key_request` out as a wildcard to-device request, + // otherwise we would have to duplicate the same content for each + // device. This is why we need to pass in "*" as the device id here. + const outgoingRequests = await this.crypto.cryptoStore.getOutgoingRoomKeyRequestsByTarget(event.getSender(), "*", [_OutgoingRoomKeyRequestManager.RoomKeyRequestState.Sent]); + return outgoingRequests.some(req => req.requestBody.room_id === roomKey.roomId && req.requestBody.session_id === roomKey.sessionId); + } + wasRoomKeyForwardedByInviter(event, roomKey) { + // TODO: This is supposed to have a time limit. We should only accept + // such keys if we happen to receive them for a recently joined room. + const room = this.baseApis.getRoom(roomKey.roomId); + const senderKey = event.getSenderKey(); + if (!senderKey) { + return false; + } + const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, senderKey); + if (!senderKeyUser) { + return false; + } + const memberEvent = room?.getMember(this.userId)?.events.member; + const fromInviter = memberEvent?.getSender() === senderKeyUser || memberEvent?.getUnsigned()?.prev_sender === senderKeyUser && memberEvent?.getPrevContent()?.membership === "invite"; + if (room && fromInviter) { + return true; + } else { + return false; + } + } + wasRoomKeyForwardedAsHistory(roomKey) { + const room = this.baseApis.getRoom(roomKey.roomId); - senderKey = content.sender_key; + // If the key is not for a known room, then something fishy is going on, + // so we reject the key out of caution. In practice, this is a bit moot + // because we'll only accept shared_history forwarded by the inviter, and + // we won't know who was the inviter for an unknown room, so we'll reject + // it anyway. + if (room && roomKey.extraSessionData.sharedHistory) { + return true; } else { - keysClaimed = event.getKeysClaimed(); + return false; } + } - if (content["org.matrix.msc3061.shared_history"]) { - extraSessionData.sharedHistory = true; + /** + * Check if a forwarded room key should be parked. + * + * A forwarded room key should be parked if it's a key for a room we're not + * in. We park the forwarded room key in case *this sender* invites us to + * that room later. + */ + shouldParkForwardedKey(roomKey) { + const room = this.baseApis.getRoom(roomKey.roomId); + if (!room && roomKey.extraSessionData.sharedHistory) { + return true; + } else { + return false; } + } + + /** + * Park the given room key to our store. + * + * @param event - An `m.forwarded_room_key` event. + * @param roomKey - The room key that was found in the event. + * + * @internal + * + */ + async parkForwardedKey(event, roomKey) { + const parkedData = { + senderId: event.getSender(), + senderKey: roomKey.senderKey, + sessionId: roomKey.sessionId, + sessionKey: roomKey.sessionKey, + keysClaimed: roomKey.keysClaimed, + forwardingCurve25519KeyChain: roomKey.forwardingKeyChain + }; + await this.crypto.cryptoStore.doTxn("readwrite", ["parked_shared_history"], txn => this.crypto.cryptoStore.addParkedSharedHistory(roomKey.roomId, parkedData, txn), _logger.logger.withPrefix("[addParkedSharedHistory]")); + } + /** + * Add the given room key to our store. + * + * @param roomKey - The room key that should be added to the store. + * + * @internal + * + */ + async addRoomKey(roomKey) { try { - await this.olmDevice.addInboundGroupSession(content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, exportFormat, extraSessionData); // have another go at decrypting events sent with this session. + await this.olmDevice.addInboundGroupSession(roomKey.roomId, roomKey.senderKey, roomKey.forwardingKeyChain, roomKey.sessionId, roomKey.sessionKey, roomKey.keysClaimed, roomKey.exportFormat, roomKey.extraSessionData); - if (await this.retryDecryption(senderKey, content.session_id, !extraSessionData.untrusted)) { + // have another go at decrypting events sent with this session. + if (await this.retryDecryption(roomKey.senderKey, roomKey.sessionId, !roomKey.extraSessionData.untrusted)) { // cancel any outstanding room key requests for this session. // Only do this if we managed to decrypt every message in the // session, because if we didn't, we leave the other key // requests in the hopes that someone sends us a key that // includes an earlier index. this.crypto.cancelRoomKeyRequest({ - algorithm: content.algorithm, - room_id: content.room_id, - session_id: content.session_id, - sender_key: senderKey + algorithm: roomKey.algorithm, + room_id: roomKey.roomId, + session_id: roomKey.sessionId, + sender_key: roomKey.senderKey }); - } // don't wait for the keys to be backed up for the server - + } - await this.crypto.backupManager.backupGroupSession(senderKey, content.session_id); + // don't wait for the keys to be backed up for the server + await this.crypto.backupManager.backupGroupSession(roomKey.senderKey, roomKey.sessionId); } catch (e) { - _logger.logger.error(`Error handling m.room_key_event: ${e}`); + this.prefixedLogger.error(`Error handling m.room_key_event: ${e}`); } } + /** - * @inheritdoc + * Handle room keys that have been forwarded to us as an + * `m.forwarded_room_key` event. + * + * Forwarded room keys need special handling since we have no way of knowing + * who the original creator of the room key was. This naturally means that + * forwarded room keys are always untrusted and should only be accepted in + * some cases. + * + * @param event - An `m.forwarded_room_key` event. + * + * @internal * - * @param {module:models/event.MatrixEvent} event key event */ + async onForwardedRoomKey(event) { + const roomKey = this.forwardedRoomKeyFromEvent(event); + if (!roomKey) { + return; + } + if (await this.shouldAcceptForwardedKey(event, roomKey)) { + await this.addRoomKey(roomKey); + } else if (this.shouldParkForwardedKey(roomKey)) { + await this.parkForwardedKey(event, roomKey); + } + } + async onRoomKeyEvent(event) { + if (event.getType() == "m.forwarded_room_key") { + await this.onForwardedRoomKey(event); + } else { + const roomKey = this.roomKeyFromEvent(event); + if (!roomKey) { + return; + } + await this.addRoomKey(roomKey); + } + } - + /** + * @param event - key event + */ async onRoomKeyWithheldEvent(event) { const content = event.getContent(); const senderKey = content.sender_key; - if (content.code === "m.no_olm") { - const sender = event.getSender(); + await this.onNoOlmWithheldEvent(event); + } else if (content.code === "m.unavailable") { + // this simply means that the other device didn't have the key, which isn't very useful information. Don't + // record it in the storage + } else { + await this.olmDevice.addInboundGroupSessionWithheld(content.room_id, senderKey, content.session_id, content.code, content.reason); + } - _logger.logger.warn(`${sender}:${senderKey} was unable to establish an olm session with us`); // if the sender says that they haven't been able to establish an olm - // session, let's proactively establish one - // Note: after we record that the olm session has had a problem, we - // trigger retrying decryption for all the messages from the sender's + // Having recorded the problem, retry decryption on any affected messages. + // It's unlikely we'll be able to decrypt sucessfully now, but this will + // update the error message. + // + if (content.session_id) { + await this.retryDecryption(senderKey, content.session_id); + } else { + // no_olm messages aren't specific to a given megolm session, so + // we trigger retrying decryption for all the messages from the sender's // key, so that we can update the error message to indicate the olm // session problem. - - - if (await this.olmDevice.getSessionIdForDevice(senderKey)) { - // a session has already been established, so we don't need to - // create a new one. - _logger.logger.debug("New session already created. Not creating a new one."); - - await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true); - this.retryDecryptionFromSender(senderKey); - return; - } - - let device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey); - + await this.retryDecryptionFromSender(senderKey); + } + } + async onNoOlmWithheldEvent(event) { + const content = event.getContent(); + const senderKey = content.sender_key; + const sender = event.getSender(); + this.prefixedLogger.warn(`${sender}:${senderKey} was unable to establish an olm session with us`); + // if the sender says that they haven't been able to establish an olm + // session, let's proactively establish one + + if (await this.olmDevice.getSessionIdForDevice(senderKey)) { + // a session has already been established, so we don't need to + // create a new one. + this.prefixedLogger.debug("New session already created. Not creating a new one."); + await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true); + return; + } + let device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey); + if (!device) { + // if we don't know about the device, fetch the user's devices again + // and retry before giving up + await this.crypto.downloadKeys([sender], false); + device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey); if (!device) { - // if we don't know about the device, fetch the user's devices again - // and retry before giving up - await this.crypto.downloadKeys([sender], false); - device = this.crypto.deviceList.getDeviceByIdentityKey(content.algorithm, senderKey); - - if (!device) { - _logger.logger.info("Couldn't find device for identity key " + senderKey + ": not establishing session"); - - await this.olmDevice.recordSessionProblem(senderKey, "no_olm", false); - this.retryDecryptionFromSender(senderKey); - return; - } + this.prefixedLogger.info("Couldn't find device for identity key " + senderKey + ": not establishing session"); + await this.olmDevice.recordSessionProblem(senderKey, "no_olm", false); + return; } - - await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, { - [sender]: [device] - }, false); - const encryptedContent = { - algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, - ciphertext: {} - }; - await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this.userId, undefined, this.olmDevice, sender, device, { - type: "m.dummy" - }); - await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true); - this.retryDecryptionFromSender(senderKey); - await this.baseApis.sendToDevice("m.room.encrypted", { - [sender]: { - [device.deviceId]: encryptedContent - } - }); - } else { - await this.olmDevice.addInboundGroupSessionWithheld(content.room_id, senderKey, content.session_id, content.code, content.reason); } - } - /** - * @inheritdoc - */ + // XXX: switch this to use encryptAndSendToDevices() rather than duplicating it? + await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[sender, [device]]]), false); + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this.olmDevice.deviceCurve25519Key, + ciphertext: {}, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() + }; + await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this.userId, undefined, this.olmDevice, sender, device, { + type: "m.dummy" + }); + await this.olmDevice.recordSessionProblem(senderKey, "no_olm", true); + await this.baseApis.sendToDevice("m.room.encrypted", new Map([[sender, new Map([[device.deviceId, encryptedContent]])]])); + } hasKeysForKeyRequest(keyRequest) { const body = keyRequest.requestBody; - return this.olmDevice.hasInboundSessionKeys(body.room_id, body.sender_key, body.session_id // TODO: ratchet index + return this.olmDevice.hasInboundSessionKeys(body.room_id, body.sender_key, body.session_id + // TODO: ratchet index ); } - /** - * @inheritdoc - */ - shareKeysWithDevice(keyRequest) { const userId = keyRequest.userId; const deviceId = keyRequest.deviceId; const deviceInfo = this.crypto.getStoredDevice(userId, deviceId); const body = keyRequest.requestBody; - this.olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, { - [userId]: [deviceInfo] - }).then(devicemap => { - const olmSessionResult = devicemap[userId][deviceId]; - if (!olmSessionResult.sessionId) { + // XXX: switch this to use encryptAndSendToDevices()? + + this.olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[userId, [deviceInfo]]])).then(devicemap => { + const olmSessionResult = devicemap.get(userId)?.get(deviceId); + if (!olmSessionResult?.sessionId) { // no session with this device, probably because there // were no one-time keys. // @@ -1471,28 +1493,21 @@ // so just skip it. return null; } - - _logger.logger.log("sharing keys for session " + body.sender_key + "|" + body.session_id + " with device " + userId + ":" + deviceId); - + this.prefixedLogger.log("sharing keys for session " + body.sender_key + "|" + body.session_id + " with device " + userId + ":" + deviceId); return this.buildKeyForwardingMessage(body.room_id, body.sender_key, body.session_id); }).then(payload => { const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, - ciphertext: {} + ciphertext: {}, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() }; return this.olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this.userId, undefined, this.olmDevice, userId, deviceInfo, payload).then(() => { - const contentMap = { - [userId]: { - [deviceId]: encryptedContent - } - }; // TODO: retries - - return this.baseApis.sendToDevice("m.room.encrypted", contentMap); + // TODO: retries + return this.baseApis.sendToDevice("m.room.encrypted", new Map([[userId, new Map([[deviceId, encryptedContent]])]])); }); }); } - async buildKeyForwardingMessage(roomId, senderKey, sessionId) { const key = await this.olmDevice.getInboundGroupSessionKey(roomId, senderKey, sessionId); return { @@ -1510,166 +1525,144 @@ } }; } + /** - * @inheritdoc - * - * @param {module:crypto/OlmDevice.MegolmSessionData} session - * @param {object} [opts={}] options for the import - * @param {boolean} [opts.untrusted] whether the key should be considered as untrusted - * @param {string} [opts.source] where the key came from + * @param untrusted - whether the key should be considered as untrusted + * @param source - where the key came from */ - - - importRoomKey(session, opts = {}) { + importRoomKey(session, { + untrusted, + source + } = {}) { const extraSessionData = {}; - - if (opts.untrusted || session.untrusted) { + if (untrusted || session.untrusted) { extraSessionData.untrusted = true; } - if (session["org.matrix.msc3061.shared_history"]) { extraSessionData.sharedHistory = true; } - return this.olmDevice.addInboundGroupSession(session.room_id, session.sender_key, session.forwarding_curve25519_key_chain, session.session_id, session.session_key, session.sender_claimed_keys, true, extraSessionData).then(() => { - if (opts.source !== "backup") { + if (source !== "backup") { // don't wait for it to complete this.crypto.backupManager.backupGroupSession(session.sender_key, session.session_id).catch(e => { // This throws if the upload failed, but this is fine // since it will have written it to the db and will retry. - _logger.logger.log("Failed to back up megolm session", e); + this.prefixedLogger.log("Failed to back up megolm session", e); }); - } // have another go at decrypting events sent with this session. - - + } + // have another go at decrypting events sent with this session. this.retryDecryption(session.sender_key, session.session_id, !extraSessionData.untrusted); }); } + /** * Have another go at decrypting events after we receive a key. Resolves once * decryption has been re-attempted on all events. * - * @private - * @param {String} senderKey - * @param {String} sessionId - * @param {Boolean} keyTrusted + * @internal + * @param forceRedecryptIfUntrusted - whether messages that were already + * successfully decrypted using untrusted keys should be re-decrypted * - * @return {Boolean} whether all messages were successfully + * @returns whether all messages were successfully * decrypted with trusted keys */ - - - async retryDecryption(senderKey, sessionId, keyTrusted) { + async retryDecryption(senderKey, sessionId, forceRedecryptIfUntrusted) { const senderPendingEvents = this.pendingEvents.get(senderKey); - if (!senderPendingEvents) { return true; } - const pending = senderPendingEvents.get(sessionId); - if (!pending) { return true; } - - _logger.logger.debug("Retrying decryption on events", [...pending]); - - await Promise.all([...pending].map(async ev => { + const pendingList = [...pending]; + this.prefixedLogger.debug("Retrying decryption on events:", pendingList.map(e => `${e.getId()}`)); + await Promise.all(pendingList.map(async ev => { try { await ev.attemptDecryption(this.crypto, { isRetry: true, - keyTrusted + forceRedecryptIfUntrusted }); - } catch (e) {// don't die if something goes wrong + } catch (e) { + // don't die if something goes wrong } - })); // If decrypted successfully with trusted keys, they'll have - // been removed from pendingEvents + })); + // If decrypted successfully with trusted keys, they'll have + // been removed from pendingEvents return !this.pendingEvents.get(senderKey)?.has(sessionId); } - async retryDecryptionFromSender(senderKey) { const senderPendingEvents = this.pendingEvents.get(senderKey); - if (!senderPendingEvents) { return true; } - this.pendingEvents.delete(senderKey); await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => { await Promise.all([...pending].map(async ev => { try { await ev.attemptDecryption(this.crypto); - } catch (e) {// don't die if something goes wrong + } catch (e) { + // don't die if something goes wrong } })); })); return !this.pendingEvents.has(senderKey); } - async sendSharedHistoryInboundSessions(devicesByUser) { await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser); - - _logger.logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser)); - const sharedHistorySessions = await this.olmDevice.getSharedHistoryInboundGroupSessions(this.roomId); - - _logger.logger.log("shared-history sessions", sharedHistorySessions); - + this.prefixedLogger.log(`Sharing history in with users ${Array.from(devicesByUser.keys())}`, sharedHistorySessions.map(([senderKey, sessionId]) => `${senderKey}|${sessionId}`)); for (const [senderKey, sessionId] of sharedHistorySessions) { const payload = await this.buildKeyForwardingMessage(this.roomId, senderKey, sessionId); - const promises = []; - const contentMap = {}; - - for (const [userId, devices] of Object.entries(devicesByUser)) { - contentMap[userId] = {}; + // FIXME: use encryptAndSendToDevices() rather than duplicating it here. + const promises = []; + const contentMap = new Map(); + for (const [userId, devices] of devicesByUser) { + const deviceMessages = new Map(); + contentMap.set(userId, deviceMessages); for (const deviceInfo of devices) { const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, - ciphertext: {} + ciphertext: {}, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() }; - contentMap[userId][deviceInfo.deviceId] = encryptedContent; + deviceMessages.set(deviceInfo.deviceId, encryptedContent); promises.push(olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this.userId, undefined, this.olmDevice, userId, deviceInfo, payload)); } } + await Promise.all(promises); - await Promise.all(promises); // prune out any devices that encryptMessageForDevice could not encrypt for, + // prune out any devices that encryptMessageForDevice could not encrypt for, // in which case it will have just not added anything to the ciphertext object. // There's no point sending messages to devices if we couldn't encrypt to them, // since that's effectively a blank message. - - for (const userId of Object.keys(contentMap)) { - for (const deviceId of Object.keys(contentMap[userId])) { - if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) { - _logger.logger.log("No ciphertext for device " + userId + ":" + deviceId + ": pruning"); - - delete contentMap[userId][deviceId]; + for (const [userId, deviceMessages] of contentMap) { + for (const [deviceId, content] of deviceMessages) { + if (!hasCiphertext(content)) { + this.prefixedLogger.log("No ciphertext for device " + userId + ":" + deviceId + ": pruning"); + deviceMessages.delete(deviceId); } - } // No devices left for that user? Strip that too. - - - if (Object.keys(contentMap[userId]).length === 0) { - _logger.logger.log("Pruned all devices for user " + userId); - - delete contentMap[userId]; } - } // Is there anything left? - - - if (Object.keys(contentMap).length === 0) { - _logger.logger.log("No users left to send to: aborting"); + // No devices left for that user? Strip that too. + if (deviceMessages.size === 0) { + this.prefixedLogger.log("Pruned all devices for user " + userId); + contentMap.delete(userId); + } + } + // Is there anything left? + if (contentMap.size === 0) { + this.prefixedLogger.log("No users left to send to: aborting"); return; } - await this.baseApis.sendToDevice("m.room.encrypted", contentMap); } } - } - +exports.MegolmDecryption = MegolmDecryption; const PROBLEM_DESCRIPTIONS = { no_olm: "The sender was unable to establish a secure channel.", unknown: "The secure channel with the sender was corrupted." diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/olm.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/olm.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/olm.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/algorithms/olm.js 2023-04-11 06:11:52.000000000 +0000 @@ -1,55 +1,40 @@ "use strict"; var _logger = require("../../logger"); - var olmlib = _interopRequireWildcard(require("../olmlib")); - var _deviceinfo = require("../deviceinfo"); - var _base = require("./base"); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const DeviceVerification = _deviceinfo.DeviceInfo.DeviceVerification; - /** * Olm encryption implementation * - * @constructor - * @extends {module:crypto/algorithms/EncryptionAlgorithm} - * - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/EncryptionAlgorithm} + * @param params - parameters, as per {@link EncryptionAlgorithm} */ class OlmEncryption extends _base.EncryptionAlgorithm { constructor(...args) { super(...args); - _defineProperty(this, "sessionPrepared", false); - _defineProperty(this, "prepPromise", null); } - /** - * @private - * @param {string[]} roomMembers list of currently-joined users in the room - * @return {Promise} Promise which resolves when setup is complete + * @internal + * @param roomMembers - list of currently-joined users in the room + * @returns Promise which resolves when setup is complete */ ensureSession(roomMembers) { if (this.prepPromise) { // prep already in progress return this.prepPromise; } - if (this.sessionPrepared) { // prep already done return Promise.resolve(); } - this.prepPromise = this.crypto.downloadKeys(roomMembers).then(() => { return this.crypto.ensureOlmSessionsForUsers(roomMembers); }).then(() => { @@ -59,22 +44,18 @@ }); return this.prepPromise; } + /** - * @inheritdoc + * @param content - plaintext event content * - * @param {module:models/room} room - * @param {string} eventType - * @param {object} content plaintext event content - * - * @return {Promise} Promise which resolves to the new event body + * @returns Promise which resolves to the new event body */ - - async encryptMessage(room, eventType, content) { // pick the list of recipients based on the membership list. // // TODO: there is a race condition here! What if a new user turns up // just as you are sending a secret message? + const members = await room.getEncryptionTargetMembers(); const users = members.map(function (u) { return u.userId; @@ -91,51 +72,34 @@ ciphertext: {} }; const promises = []; - - for (let i = 0; i < users.length; ++i) { - const userId = users[i]; + for (const userId of users) { const devices = this.crypto.getStoredDevicesForUser(userId) || []; - - for (let j = 0; j < devices.length; ++j) { - const deviceInfo = devices[j]; + for (const deviceInfo of devices) { const key = deviceInfo.getIdentityKey(); - if (key == this.olmDevice.deviceCurve25519Key) { // don't bother sending to ourself continue; } - if (deviceInfo.verified == DeviceVerification.BLOCKED) { // don't bother setting up sessions with blocked users continue; } - promises.push(olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this.userId, this.deviceId, this.olmDevice, userId, deviceInfo, payloadFields)); } } - return Promise.all(promises).then(() => encryptedContent); } - } + /** * Olm decryption implementation * - * @constructor - * @extends {module:crypto/algorithms/DecryptionAlgorithm} - * @param {object} params parameters, as per - * {@link module:crypto/algorithms/DecryptionAlgorithm} + * @param params - parameters, as per {@link DecryptionAlgorithm} */ - - class OlmDecryption extends _base.DecryptionAlgorithm { /** - * @inheritdoc - * - * @param {MatrixEvent} event - * * returns a promise which resolves to a - * {@link module:crypto~EventDecryptionResult} once we have finished + * {@link EventDecryptionResult} once we have finished * decrypting. Rejects with an `algorithms.DecryptionError` if there is a * problem decrypting the event. */ @@ -143,18 +107,14 @@ const content = event.getWireContent(); const deviceKey = content.sender_key; const ciphertext = content.ciphertext; - if (!ciphertext) { throw new _base.DecryptionError("OLM_MISSING_CIPHERTEXT", "Missing ciphertext"); } - if (!(this.olmDevice.deviceCurve25519Key in ciphertext)) { throw new _base.DecryptionError("OLM_NOT_INCLUDED_IN_RECIPIENTS", "Not included in recipients"); } - const message = ciphertext[this.olmDevice.deviceCurve25519Key]; let payloadString; - try { payloadString = await this.decryptMessage(deviceKey, message); } catch (e) { @@ -163,53 +123,50 @@ err: e }); } + const payload = JSON.parse(payloadString); - const payload = JSON.parse(payloadString); // check that we were the intended recipient, to avoid unknown-key attack + // check that we were the intended recipient, to avoid unknown-key attack // https://github.com/vector-im/vector-web/issues/2483 - if (payload.recipient != this.userId) { throw new _base.DecryptionError("OLM_BAD_RECIPIENT", "Message was intented for " + payload.recipient); } - if (payload.recipient_keys.ed25519 != this.olmDevice.deviceEd25519Key) { throw new _base.DecryptionError("OLM_BAD_RECIPIENT_KEY", "Message not intended for this device", { intended: payload.recipient_keys.ed25519, our_key: this.olmDevice.deviceEd25519Key }); - } // check that the device that encrypted the event belongs to the user + } + + // check that the device that encrypted the event belongs to the user // that the event claims it's from. We need to make sure that our // device list is up-to-date. If the device is unknown, we can only // assume that the device logged out. Some event handlers, such as // secret sharing, may be more strict and reject events that come from // unknown devices. - - await this.crypto.deviceList.downloadKeys([event.getSender()], false); const senderKeyUser = this.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, deviceKey); - - if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined) { + if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) { throw new _base.DecryptionError("OLM_BAD_SENDER", "Message claimed to be from " + event.getSender(), { real_sender: senderKeyUser }); - } // check that the original sender matches what the homeserver told us, to + } + + // check that the original sender matches what the homeserver told us, to // avoid people masquerading as others. // (this check is also provided via the sender's embedded ed25519 key, // which is checked elsewhere). - - if (payload.sender != event.getSender()) { throw new _base.DecryptionError("OLM_FORWARDED_MESSAGE", "Message forwarded from " + payload.sender, { reported_sender: event.getSender() }); - } // Olm events intended for a room have a room_id. - + } + // Olm events intended for a room have a room_id. if (payload.room_id !== event.getRoomId()) { throw new _base.DecryptionError("OLM_BAD_ROOM", "Message intended for room " + payload.room_id, { reported_room: event.getRoomId() || "ROOM_ID_UNDEFINED" }); } - const claimedKeys = payload.keys || {}; return { clearEvent: payload, @@ -217,16 +174,15 @@ claimedEd25519Key: claimedKeys.ed25519 || null }; } + /** * Attempt to decrypt an Olm message * - * @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender - * @param {object} message message object, with 'type' and 'body' fields + * @param theirDeviceIdentityKey - Curve25519 identity key of the sender + * @param message - message object, with 'type' and 'body' fields * - * @return {string} payload, if decrypted successfully. + * @returns payload, if decrypted successfully. */ - - decryptMessage(theirDeviceIdentityKey, message) { // This is a wrapper that serialises decryptions of prekey messages, because // otherwise we race between deciding we have no active sessions for the message @@ -237,68 +193,57 @@ } else { const myPromise = this.olmDevice.olmPrekeyPromise.then(() => { return this.reallyDecryptMessage(theirDeviceIdentityKey, message); - }); // we want the error, but don't propagate it to the next decryption - + }); + // we want the error, but don't propagate it to the next decryption this.olmDevice.olmPrekeyPromise = myPromise.catch(() => {}); return myPromise; } } - async reallyDecryptMessage(theirDeviceIdentityKey, message) { - const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey); // try each session in turn. + const sessionIds = await this.olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey); + // try each session in turn. const decryptionErrors = {}; - - for (let i = 0; i < sessionIds.length; i++) { - const sessionId = sessionIds[i]; - + for (const sessionId of sessionIds) { try { const payload = await this.olmDevice.decryptMessage(theirDeviceIdentityKey, sessionId, message.type, message.body); - _logger.logger.log("Decrypted Olm message from " + theirDeviceIdentityKey + " with session " + sessionId); - return payload; } catch (e) { const foundSession = await this.olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, message.type, message.body); - if (foundSession) { // decryption failed, but it was a prekey message matching this // session, so it should have worked. throw new Error("Error decrypting prekey message with existing session id " + sessionId + ": " + e.message); - } // otherwise it's probably a message for another session; carry on, but - // keep a record of the error - + } + // otherwise it's probably a message for another session; carry on, but + // keep a record of the error decryptionErrors[sessionId] = e.message; } } - if (message.type !== 0) { // not a prekey message, so it should have matched an existing session, but it // didn't work. + if (sessionIds.length === 0) { throw new Error("No existing sessions"); } - throw new Error("Error decrypting non-prekey message with existing sessions: " + JSON.stringify(decryptionErrors)); - } // prekey message which doesn't match any existing sessions: make a new - // session. + } + // prekey message which doesn't match any existing sessions: make a new + // session. let res; - try { res = await this.olmDevice.createInboundSession(theirDeviceIdentityKey, message.type, message.body); } catch (e) { decryptionErrors["(new)"] = e.message; throw new Error("Error decrypting prekey message: " + JSON.stringify(decryptionErrors)); } - _logger.logger.log("created new inbound Olm session ID " + res.session_id + " with " + theirDeviceIdentityKey); - return res.payload; } - } - (0, _base.registerAlgorithm)(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/api.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/api.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/api.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/api.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,7 +4,6 @@ value: true }); exports.CrossSigningKey = void 0; - /* Copyright 2021 The Matrix.org Foundation C.I.C. @@ -23,7 +22,6 @@ // TODO: Merge this with crypto.js once converted let CrossSigningKey; exports.CrossSigningKey = CrossSigningKey; - (function (CrossSigningKey) { CrossSigningKey["Master"] = "master"; CrossSigningKey["SelfSigning"] = "self_signing"; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/backup.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/backup.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/backup.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/backup.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,29 +4,21 @@ value: true }); exports.algorithmsByName = exports.DefaultAlgorithm = exports.Curve25519 = exports.BackupManager = exports.Aes256 = void 0; - var _client = require("../client"); - var _logger = require("../logger"); - var _olmlib = require("./olmlib"); - var _key_passphrase = require("./key_passphrase"); - var _utils = require("../utils"); - var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store"); - var _recoverykey = require("./recoverykey"); - var _aes = require("./aes"); - var _NamespacedValue = require("../NamespacedValue"); - var _index = require("./index"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +var _crypto = require("./crypto"); +var _httpApi = require("../http-api"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms @@ -38,103 +30,81 @@ // Have we checked the server for a backup we can use? // Are we currently sending backups? // When did we last try to check the server for a given session id? + constructor(baseApis, getKey) { this.baseApis = baseApis; this.getKey = getKey; - _defineProperty(this, "algorithm", void 0); - _defineProperty(this, "backupInfo", void 0); - _defineProperty(this, "checkedForBackup", void 0); - _defineProperty(this, "sendingBackups", void 0); - _defineProperty(this, "sessionLastCheckAttemptedTime", {}); - this.checkedForBackup = false; this.sendingBackups = false; } - get version() { return this.backupInfo && this.backupInfo.version; } + /** * Performs a quick check to ensure that the backup info looks sane. * * Throws an error if a problem is detected. * - * @param {IKeyBackupInfo} info the key backup info + * @param info - the key backup info */ - - static checkBackupVersion(info) { const Algorithm = algorithmsByName[info.algorithm]; - if (!Algorithm) { throw new Error("Unknown backup algorithm: " + info.algorithm); } - if (typeof info.auth_data !== "object") { throw new Error("Invalid backup data returned"); } - return Algorithm.checkBackupVersion(info); } - static makeAlgorithm(info, getKey) { const Algorithm = algorithmsByName[info.algorithm]; - if (!Algorithm) { throw new Error("Unknown backup algorithm"); } - return Algorithm.init(info.auth_data, getKey); } - async enableKeyBackup(info) { this.backupInfo = info; - if (this.algorithm) { this.algorithm.free(); } - this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); - this.baseApis.emit(_index.CryptoEvent.KeyBackupStatus, true); // There may be keys left over from a partially completed backup, so - // schedule a send to check. + this.baseApis.emit(_index.CryptoEvent.KeyBackupStatus, true); + // There may be keys left over from a partially completed backup, so + // schedule a send to check. this.scheduleKeyBackupSend(); } + /** * Disable backing up of keys. */ - - disableKeyBackup() { if (this.algorithm) { this.algorithm.free(); } - this.algorithm = undefined; this.backupInfo = undefined; this.baseApis.emit(_index.CryptoEvent.KeyBackupStatus, false); } - getKeyBackupEnabled() { if (!this.checkedForBackup) { return null; } - return Boolean(this.algorithm); } - async prepareKeyBackupVersion(key, algorithm) { const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm; - if (!Algorithm) { throw new Error("Unknown backup algorithm"); } - const [privateKey, authData] = await Algorithm.prepare(key); const recoveryKey = (0, _recoverykey.encodeRecoveryKey)(privateKey); return { @@ -144,241 +114,191 @@ privateKey }; } - async createKeyBackupVersion(info) { this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey); } + /** * Check the server for an active key backup and * if one is present and has a valid signature from * one of the user's verified devices, start backing up * to it. */ - - async checkAndStart() { _logger.logger.log("Checking key backup status..."); - if (this.baseApis.isGuest()) { _logger.logger.log("Skipping key backup check since user is guest"); - this.checkedForBackup = true; return null; } - let backupInfo; - try { - backupInfo = await this.baseApis.getKeyBackupVersion(); + backupInfo = (await this.baseApis.getKeyBackupVersion()) ?? undefined; } catch (e) { _logger.logger.log("Error checking for active key backup", e); - if (e.httpStatus === 404) { // 404 is returned when the key backup does not exist, so that // counts as successfully checking. this.checkedForBackup = true; } - return null; } - this.checkedForBackup = true; const trustInfo = await this.isKeyBackupTrusted(backupInfo); - if (trustInfo.usable && !this.backupInfo) { - _logger.logger.log("Found usable key backup v" + backupInfo.version + ": enabling key backups"); - + _logger.logger.log(`Found usable key backup v${backupInfo.version}: enabling key backups`); await this.enableKeyBackup(backupInfo); } else if (!trustInfo.usable && this.backupInfo) { _logger.logger.log("No usable key backup: disabling key backup"); - this.disableKeyBackup(); } else if (!trustInfo.usable && !this.backupInfo) { _logger.logger.log("No usable key backup: not enabling key backup"); } else if (trustInfo.usable && this.backupInfo) { // may not be the same version: if not, we should switch if (backupInfo.version !== this.backupInfo.version) { - _logger.logger.log("On backup version " + this.backupInfo.version + " but found " + "version " + backupInfo.version + ": switching."); - + _logger.logger.log(`On backup version ${this.backupInfo.version} but ` + `found version ${backupInfo.version}: switching.`); this.disableKeyBackup(); - await this.enableKeyBackup(backupInfo); // We're now using a new backup, so schedule all the keys we have to be + await this.enableKeyBackup(backupInfo); + // We're now using a new backup, so schedule all the keys we have to be // uploaded to the new backup. This is a bit of a workaround to upload // keys to a new backup in *most* cases, but it won't cover all cases // because we don't remember what backup version we uploaded keys to: // see https://github.com/vector-im/element-web/issues/14833 - await this.scheduleAllGroupSessionsForBackup(); } else { - _logger.logger.log("Backup version " + backupInfo.version + " still current"); + _logger.logger.log(`Backup version ${backupInfo.version} still current`); } } - return { backupInfo, trustInfo }; } + /** * Forces a re-check of the key backup and enables/disables it * as appropriate. * - * @return {Object} Object with backup info (as returned by + * @returns Object with backup info (as returned by * getKeyBackupVersion) in backupInfo and * trust information (as returned by isKeyBackupTrusted) * in trustInfo. */ - - async checkKeyBackup() { this.checkedForBackup = false; return this.checkAndStart(); } + /** * Attempts to retrieve a session from a key backup, if enough time * has elapsed since the last check for this session id. */ - - async queryKeyBackupRateLimited(targetRoomId, targetSessionId) { if (!this.backupInfo) { return; } - const now = new Date().getTime(); - if (!this.sessionLastCheckAttemptedTime[targetSessionId] || now - this.sessionLastCheckAttemptedTime[targetSessionId] > KEY_BACKUP_CHECK_RATE_LIMIT) { this.sessionLastCheckAttemptedTime[targetSessionId] = now; await this.baseApis.restoreKeyBackupWithCache(targetRoomId, targetSessionId, this.backupInfo, {}); } } + /** * Check if the given backup info is trusted. * - * @param {IKeyBackupInfo} backupInfo key backup info dict from /room_keys/version - * @return {object} { - * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device - * sigs: [ - * valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation - * deviceId: [string], - * device: [DeviceInfo || null], - * ] - * } + * @param backupInfo - key backup info dict from /room_keys/version */ - - async isKeyBackupTrusted(backupInfo) { const ret = { usable: false, trusted_locally: false, sigs: [] }; - if (!backupInfo || !backupInfo.algorithm || !backupInfo.auth_data || !backupInfo.auth_data.signatures) { _logger.logger.info("Key backup is absent or missing required data"); - return ret; } - + const userId = this.baseApis.getUserId(); const privKey = await this.baseApis.crypto.getSessionBackupPrivateKey(); - if (privKey) { - let algorithm; - + let algorithm = null; try { algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey); - if (await algorithm.keyMatches(privKey)) { _logger.logger.info("Backup is trusted locally"); - ret.trusted_locally = true; } - } catch {// do nothing -- if we have an error, then we don't mark it as + } catch { + // do nothing -- if we have an error, then we don't mark it as // locally trusted } finally { - if (algorithm) { - algorithm.free(); - } + algorithm?.free(); } } - - const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {}; - + const mySigs = backupInfo.auth_data.signatures[userId] || {}; for (const keyId of Object.keys(mySigs)) { - const keyIdParts = keyId.split(':'); - - if (keyIdParts[0] !== 'ed25519') { + const keyIdParts = keyId.split(":"); + if (keyIdParts[0] !== "ed25519") { _logger.logger.log("Ignoring unknown signature type: " + keyIdParts[0]); - continue; - } // Could be a cross-signing master key, but just say this is the device + } + // Could be a cross-signing master key, but just say this is the device // ID for backwards compat - - const sigInfo = { deviceId: keyIdParts[1] - }; // first check to see if it's from our cross-signing key + }; + // first check to see if it's from our cross-signing key const crossSigningId = this.baseApis.crypto.crossSigningInfo.getId(); - if (crossSigningId === sigInfo.deviceId) { sigInfo.crossSigningId = true; - try { - await (0, _olmlib.verifySignature)(this.baseApis.crypto.olmDevice, backupInfo.auth_data, this.baseApis.getUserId(), sigInfo.deviceId, crossSigningId); + await (0, _olmlib.verifySignature)(this.baseApis.crypto.olmDevice, backupInfo.auth_data, userId, sigInfo.deviceId, crossSigningId); sigInfo.valid = true; } catch (e) { _logger.logger.warn("Bad signature from cross signing key " + crossSigningId, e); - sigInfo.valid = false; } - ret.sigs.push(sigInfo); continue; - } // Now look for a sig from a device + } + + // Now look for a sig from a device // At some point this can probably go away and we'll just support // it being signed by the cross-signing master key - - - const device = this.baseApis.crypto.deviceList.getStoredDevice(this.baseApis.getUserId(), sigInfo.deviceId); - + const device = this.baseApis.crypto.deviceList.getStoredDevice(userId, sigInfo.deviceId); if (device) { sigInfo.device = device; - sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(this.baseApis.getUserId(), sigInfo.deviceId); - + sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(userId, sigInfo.deviceId); try { - await (0, _olmlib.verifySignature)(this.baseApis.crypto.olmDevice, backupInfo.auth_data, this.baseApis.getUserId(), device.deviceId, device.getFingerprint()); + await (0, _olmlib.verifySignature)(this.baseApis.crypto.olmDevice, backupInfo.auth_data, userId, device.deviceId, device.getFingerprint()); sigInfo.valid = true; } catch (e) { _logger.logger.info("Bad signature from key ID " + keyId + " userID " + this.baseApis.getUserId() + " device ID " + device.deviceId + " fingerprint: " + device.getFingerprint(), backupInfo.auth_data, e); - sigInfo.valid = false; } } else { sigInfo.valid = null; // Can't determine validity because we don't have the signing device - _logger.logger.info("Ignoring signature from unknown key " + keyId); } - ret.sigs.push(sigInfo); } - ret.usable = ret.sigs.some(s => { - return s.valid && (s.device && s.deviceTrust.isVerified() || s.crossSigningId); + return s.valid && (s.device && s.deviceTrust?.isVerified() || s.crossSigningId); }); return ret; } + /** * Schedules sending all keys waiting to be sent to the backup, if not already * scheduled. Retries if necessary. * - * @param maxDelay Maximum delay to wait in ms. 0 means no delay. + * @param maxDelay - Maximum delay to wait in ms. 0 means no delay. */ - - async scheduleKeyBackupSend(maxDelay = 10000) { if (this.sendingBackups) return; this.sendingBackups = true; - try { // wait between 0 and `maxDelay` seconds, to avoid backup // requests from different clients hitting the server all at @@ -386,39 +306,32 @@ const delay = Math.random() * maxDelay; await (0, _utils.sleep)(delay); let numFailures = 0; // number of consecutive failures - for (;;) { if (!this.algorithm) { return; } - try { const numBackedUp = await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST); - if (numBackedUp === 0) { // no sessions left needing backup: we're done return; } - numFailures = 0; } catch (err) { numFailures++; - _logger.logger.log("Key backup request failed", err); - if (err.data) { - if (err.data.errcode == 'M_NOT_FOUND' || err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION') { + if (err.data.errcode == "M_NOT_FOUND" || err.data.errcode == "M_WRONG_ROOM_KEYS_VERSION") { // Re-check key backup status on error, so we can be // sure to present the current situation when asked. - await this.checkKeyBackup(); // Backup version has changed or this backup version + await this.checkKeyBackup(); + // Backup version has changed or this backup version // has been deleted - this.baseApis.crypto.emit(_index.CryptoEvent.KeyBackupFailed, err.data.errcode); throw err; } } } - if (numFailures) { // exponential backoff if we have failures await (0, _utils.sleep)(1000 * Math.pow(2, Math.min(numFailures - 1, 4))); @@ -428,49 +341,40 @@ this.sendingBackups = false; } } + /** * Take some e2e keys waiting to be backed up and send them * to the backup. * - * @param {number} limit Maximum number of keys to back up - * @returns {number} Number of sessions backed up + * @param limit - Maximum number of keys to back up + * @returns Number of sessions backed up */ - - async backupPendingKeys(limit) { const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit); - if (!sessions.length) { return 0; } - let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); this.baseApis.crypto.emit(_index.CryptoEvent.KeyBackupSessionsRemaining, remaining); const rooms = {}; - for (const session of sessions) { const roomId = session.sessionData.room_id; - - if (rooms[roomId] === undefined) { - rooms[roomId] = { - sessions: {} - }; - } - + (0, _utils.safeSet)(rooms, roomId, rooms[roomId] || { + sessions: {} + }); const sessionData = this.baseApis.crypto.olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); sessionData.algorithm = _olmlib.MEGOLM_ALGORITHM; const forwardedCount = (sessionData.forwarding_curve25519_key_chain || []).length; const userId = this.baseApis.crypto.deviceList.getUserByIdentityKey(_olmlib.MEGOLM_ALGORITHM, session.senderKey); - const device = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(_olmlib.MEGOLM_ALGORITHM, session.senderKey); + const device = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(_olmlib.MEGOLM_ALGORITHM, session.senderKey) ?? undefined; const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified(); - rooms[roomId]['sessions'][session.sessionId] = { + (0, _utils.safeSet)(rooms[roomId]["sessions"], session.sessionId, { first_message_index: sessionData.first_known_index, forwarded_count: forwardedCount, is_verified: verified, session_data: await this.algorithm.encryptSession(sessionData) - }; + }); } - await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms }); @@ -479,44 +383,39 @@ this.baseApis.crypto.emit(_index.CryptoEvent.KeyBackupSessionsRemaining, remaining); return sessions.length; } - async backupGroupSession(senderKey, sessionId) { await this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([{ senderKey: senderKey, sessionId: sessionId }]); - if (this.backupInfo) { // don't wait for this to complete: it will delay so // happens in the background this.scheduleKeyBackupSend(); - } // if this.backupInfo is not set, then the keys will be backed up when + } + // if this.backupInfo is not set, then the keys will be backed up when // this.enableKeyBackup is called - } + /** * Marks all group sessions as needing to be backed up and schedules them to * upload in the background as soon as possible. */ - - async scheduleAllGroupSessionsForBackup() { - await this.flagAllGroupSessionsForBackup(); // Schedule keys to upload in the background as soon as possible. + await this.flagAllGroupSessionsForBackup(); - this.scheduleKeyBackupSend(0 - /* maxDelay */ - ); + // Schedule keys to upload in the background as soon as possible. + this.scheduleKeyBackupSend(0 /* maxDelay */); } + /** * Marks all group sessions as needing to be backed up without scheduling * them to upload in the background. - * @returns {Promise} Resolves to the number of sessions now requiring a backup + * @returns Promise which resolves to the number of sessions now requiring a backup * (which will be equal to the number of sessions in the store). */ - - async flagAllGroupSessionsForBackup() { - await this.baseApis.crypto.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_BACKUP], txn => { + await this.baseApis.crypto.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_BACKUP], txn => { this.baseApis.crypto.cryptoStore.getAllEndToEndInboundGroupSessions(txn, session => { if (session !== null) { this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([session], txn); @@ -527,44 +426,36 @@ this.baseApis.emit(_index.CryptoEvent.KeyBackupSessionsRemaining, remaining); return remaining; } + /** * Counts the number of end to end session keys that are waiting to be backed up - * @returns {Promise} Resolves to the number of sessions requiring backup + * @returns Promise which resolves to the number of sessions requiring backup */ - - countSessionsNeedingBackup() { return this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); } - } - exports.BackupManager = BackupManager; - class Curve25519 { - constructor(authData, publicKey, // FIXME: PkEncryption + constructor(authData, publicKey, + // FIXME: PkEncryption getKey) { this.authData = authData; this.publicKey = publicKey; this.getKey = getKey; } - static async init(authData, getKey) { if (!authData || !("public_key" in authData)) { throw new Error("auth_data missing required information"); } - const publicKey = new global.Olm.PkEncryption(); publicKey.set_recipient_key(authData.public_key); return new Curve25519(authData, publicKey, getKey); } - static async prepare(key) { const decryption = new global.Olm.PkDecryption(); - try { const authData = {}; - if (!key) { authData.public_key = decryption.generate_key(); } else if (key instanceof Uint8Array) { @@ -575,7 +466,6 @@ authData.private_key_iterations = derivation.iterations; authData.public_key = decryption.init_with_private_key(derivation.key); } - const publicKey = new global.Olm.PkEncryption(); publicKey.set_recipient_key(authData.public_key); return [decryption.get_private_key(), authData]; @@ -583,17 +473,14 @@ decryption.free(); } } - static checkBackupVersion(info) { if (!("public_key" in info.auth_data)) { throw new Error("Invalid backup data returned"); } } - get untrusted() { return true; } - async encryptSession(data) { const plainText = Object.assign({}, data); delete plainText.session_id; @@ -601,23 +488,17 @@ delete plainText.first_known_index; return this.publicKey.encrypt(JSON.stringify(plainText)); } - async decryptSessions(sessions) { const privKey = await this.getKey(); const decryption = new global.Olm.PkDecryption(); - try { const backupPubKey = decryption.init_with_private_key(privKey); - if (backupPubKey !== this.authData.public_key) { - // eslint-disable-next-line no-throw-literal - throw { + throw new _httpApi.MatrixError({ errcode: _client.MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY - }; + }); } - const keys = []; - for (const [sessionId, sessionData] of Object.entries(sessions)) { try { const decrypted = JSON.parse(decryption.decrypt(sessionData.session_data.ephemeral, sessionData.session_data.mac, sessionData.session_data.ciphertext)); @@ -627,86 +508,56 @@ _logger.logger.log("Failed to decrypt megolm session from backup", e, sessionData); } } - return keys; } finally { decryption.free(); } } - async keyMatches(key) { const decryption = new global.Olm.PkDecryption(); let pubKey; - try { pubKey = decryption.init_with_private_key(key); } finally { decryption.free(); } - return pubKey === this.authData.public_key; } - free() { this.publicKey.free(); } - } - exports.Curve25519 = Curve25519; - _defineProperty(Curve25519, "algorithmName", "m.megolm_backup.v1.curve25519-aes-sha2"); - function randomBytes(size) { - const crypto = (0, _utils.getCrypto)(); - - if (crypto) { - // nodejs version - return crypto.randomBytes(size); - } - - if (window?.crypto) { - // browser version - const buf = new Uint8Array(size); - window.crypto.getRandomValues(buf); - return buf; - } - - throw new Error("No usable crypto implementation"); + const buf = new Uint8Array(size); + _crypto.crypto.getRandomValues(buf); + return buf; } - -const UNSTABLE_MSC3270_NAME = new _NamespacedValue.UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2"); - +const UNSTABLE_MSC3270_NAME = new _NamespacedValue.UnstableValue("m.megolm_backup.v1.aes-hmac-sha2", "org.matrix.msc3270.v1.aes-hmac-sha2"); class Aes256 { constructor(authData, key) { this.authData = authData; this.key = key; } - static async init(authData, getKey) { if (!authData) { throw new Error("auth_data missing"); } - const key = await getKey(); - if (authData.mac) { const { mac } = await (0, _aes.calculateKeyCheck)(key, authData.iv); - - if (authData.mac.replace(/=+$/g, '') !== mac.replace(/=+/g, '')) { + if (authData.mac.replace(/=+$/g, "") !== mac.replace(/=+/g, "")) { throw new Error("Key does not match"); } } - return new Aes256(authData, key); } - static async prepare(key) { let outKey; const authData = {}; - if (!key) { outKey = randomBytes(32); } else if (key instanceof Uint8Array) { @@ -717,7 +568,6 @@ authData.private_key_iterations = derivation.iterations; outKey = derivation.key; } - const { iv, mac @@ -726,17 +576,14 @@ authData.mac = mac; return [outKey, authData]; } - static checkBackupVersion(info) { if (!("iv" in info.auth_data && "mac" in info.auth_data)) { throw new Error("Invalid backup data returned"); } } - get untrusted() { return false; } - encryptSession(data) { const plainText = Object.assign({}, data); delete plainText.session_id; @@ -744,10 +591,8 @@ delete plainText.first_known_index; return (0, _aes.encryptAES)(JSON.stringify(plainText), this.key, data.session_id); } - async decryptSessions(sessions) { const keys = []; - for (const [sessionId, sessionData] of Object.entries(sessions)) { try { const decrypted = JSON.parse(await (0, _aes.decryptAES)(sessionData.session_data, this.key, sessionId)); @@ -757,32 +602,25 @@ _logger.logger.log("Failed to decrypt megolm session from backup", e, sessionData); } } - return keys; } - async keyMatches(key) { if (this.authData.mac) { const { mac } = await (0, _aes.calculateKeyCheck)(key, this.authData.iv); - return this.authData.mac.replace(/=+$/g, '') === mac.replace(/=+/g, ''); + return this.authData.mac.replace(/=+$/g, "") === mac.replace(/=+/g, ""); } else { // if we have no information, we have to assume the key is right return true; } } - free() { this.key.fill(0); } - } - exports.Aes256 = Aes256; - _defineProperty(Aes256, "algorithmName", UNSTABLE_MSC3270_NAME.name); - const algorithmsByName = { [Curve25519.algorithmName]: Curve25519, [Aes256.algorithmName]: Aes256 diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/CrossSigning.js 2023-04-11 06:11:52.000000000 +0000 @@ -6,26 +6,20 @@ exports.UserTrustLevel = exports.DeviceTrustLevel = exports.CrossSigningLevel = exports.CrossSigningInfo = void 0; exports.createCryptoStoreCacheCallbacks = createCryptoStoreCacheCallbacks; exports.requestKeysDuringVerification = requestKeysDuringVerification; - var _olmlib = require("./olmlib"); - var _logger = require("../logger"); - var _indexeddbCryptoStore = require("../crypto/store/indexeddb-crypto-store"); - var _aes = require("./aes"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const KEY_REQUEST_TIMEOUT_MS = 1000 * 60; - function publicKeyFromKeyInfo(keyInfo) { // `keys` is an object with { [`ed25519:${pubKey}`]: pubKey } // We assume only a single key, and we want the bare form without type // prefix, so we select the values. return Object.values(keyInfo.keys)[0]; } - class CrossSigningInfo { // This tracks whether we've ever verified this user with any identity. // When you verify a user, any devices online at the time that receive @@ -36,37 +30,29 @@ /** * Information about a user's cross-signing keys * - * @class - * - * @param {string} userId the user that the information is about - * @param {object} callbacks Callbacks used to interact with the app + * @param userId - the user that the information is about + * @param callbacks - Callbacks used to interact with the app * Requires getCrossSigningKey and saveCrossSigningKeys - * @param {object} cacheCallbacks Callbacks used to interact with the cache + * @param cacheCallbacks - Callbacks used to interact with the cache */ constructor(userId, callbacks = {}, cacheCallbacks = {}) { this.userId = userId; this.callbacks = callbacks; this.cacheCallbacks = cacheCallbacks; - _defineProperty(this, "keys", {}); - _defineProperty(this, "firstUse", true); - _defineProperty(this, "crossSigningVerifiedBefore", false); } - static fromStorage(obj, userId) { const res = new CrossSigningInfo(userId); - for (const prop in obj) { if (obj.hasOwnProperty(prop)) { + // @ts-ignore - ts doesn't like this and nor should we res[prop] = obj[prop]; } } - return res; } - toStorage() { return { keys: this.keys, @@ -74,88 +60,72 @@ crossSigningVerifiedBefore: this.crossSigningVerifiedBefore }; } + /** * Calls the app callback to ask for a private key * - * @param {string} type The key type ("master", "self_signing", or "user_signing") - * @param {string} expectedPubkey The matching public key or undefined to use + * @param type - The key type ("master", "self_signing", or "user_signing") + * @param expectedPubkey - The matching public key or undefined to use * the stored public key for the given key type. - * @returns {Array} An array with [ public key, Olm.PkSigning ] + * @returns An array with [ public key, Olm.PkSigning ] */ - - async getCrossSigningKey(type, expectedPubkey) { const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0; - if (!this.callbacks.getCrossSigningKey) { throw new Error("No getCrossSigningKey callback supplied"); } - if (expectedPubkey === undefined) { expectedPubkey = this.getId(type); } - function validateKey(key) { if (!key) return; const signing = new global.Olm.PkSigning(); const gotPubkey = signing.init_with_seed(key); - if (gotPubkey === expectedPubkey) { return [gotPubkey, signing]; } - signing.free(); } - - let privkey; - + let privkey = null; if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) { privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey); } - const cacheresult = validateKey(privkey); - if (cacheresult) { return cacheresult; } - privkey = await this.callbacks.getCrossSigningKey(type, expectedPubkey); const result = validateKey(privkey); - if (result) { if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey); } - return result; } - /* No keysource even returned a key */ - + /* No keysource even returned a key */ if (!privkey) { throw new Error("getCrossSigningKey callback for " + type + " returned falsey"); } - /* We got some keys from the keysource, but none of them were valid */ - + /* We got some keys from the keysource, but none of them were valid */ throw new Error("Key type " + type + " from getCrossSigningKey callback did not match"); } + /** * Check whether the private keys exist in secret storage. * XXX: This could be static, be we often seem to have an instance when we * want to know this anyway... * - * @param {SecretStorage} secretStorage The secret store using account data - * @returns {object} map of key name to key info the secret is encrypted + * @param secretStorage - The secret store using account data + * @returns map of key name to key info the secret is encrypted * with, or null if it is not present or not encrypted with a trusted * key */ - - async isStoredInSecretStorage(secretStorage) { // check what SSSS keys have encrypted the master key (if any) - const stored = (await secretStorage.isStored("m.cross_signing.master")) || {}; // then check which of those SSSS keys have also encrypted the SSK and USK - + const stored = (await secretStorage.isStored("m.cross_signing.master")) || {}; + // then check which of those SSSS keys have also encrypted the SSK and USK function intersect(s) { for (const k of Object.keys(stored)) { if (!s[k]) { @@ -163,137 +133,119 @@ } } } - for (const type of ["self_signing", "user_signing"]) { intersect((await secretStorage.isStored(`m.cross_signing.${type}`)) || {}); } - return Object.keys(stored).length ? stored : null; } + /** * Store private keys in secret storage for use by other devices. This is * typically called in conjunction with the creation of new cross-signing * keys. * - * @param {Map} keys The keys to store - * @param {SecretStorage} secretStorage The secret store using account data + * @param keys - The keys to store + * @param secretStorage - The secret store using account data */ - - static async storeInSecretStorage(keys, secretStorage) { for (const [type, privateKey] of keys) { const encodedKey = (0, _olmlib.encodeBase64)(privateKey); await secretStorage.store(`m.cross_signing.${type}`, encodedKey); } } + /** * Get private keys from secret storage created by some other device. This * also passes the private keys to the app-specific callback. * - * @param {string} type The type of key to get. One of "master", + * @param type - The type of key to get. One of "master", * "self_signing", or "user_signing". - * @param {SecretStorage} secretStorage The secret store using account data - * @return {Uint8Array} The private key + * @param secretStorage - The secret store using account data + * @returns The private key */ - - static async getFromSecretStorage(type, secretStorage) { const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); - if (!encodedKey) { return null; } - return (0, _olmlib.decodeBase64)(encodedKey); } + /** * Check whether the private keys exist in the local key cache. * - * @param {string} [type] The type of key to get. One of "master", + * @param type - The type of key to get. One of "master", * "self_signing", or "user_signing". Optional, will check all by default. - * @returns {boolean} True if all keys are stored in the local cache. + * @returns True if all keys are stored in the local cache. */ - - async isStoredInKeyCache(type) { const cacheCallbacks = this.cacheCallbacks; if (!cacheCallbacks) return false; const types = type ? [type] : ["master", "self_signing", "user_signing"]; - for (const t of types) { - if (!(await cacheCallbacks.getCrossSigningKeyCache(t))) { + if (!(await cacheCallbacks.getCrossSigningKeyCache?.(t))) { return false; } } - return true; } + /** * Get cross-signing private keys from the local cache. * - * @returns {Map} A map from key type (string) to private key (Uint8Array) + * @returns A map from key type (string) to private key (Uint8Array) */ - - async getCrossSigningKeysFromCache() { const keys = new Map(); const cacheCallbacks = this.cacheCallbacks; if (!cacheCallbacks) return keys; - for (const type of ["master", "self_signing", "user_signing"]) { - const privKey = await cacheCallbacks.getCrossSigningKeyCache(type); - + const privKey = await cacheCallbacks.getCrossSigningKeyCache?.(type); if (!privKey) { continue; } - keys.set(type, privKey); } - return keys; } + /** * Get the ID used to identify the user. This can also be used to test for * the existence of a given key type. * - * @param {string} type The type of key to get the ID of. One of "master", + * @param type - The type of key to get the ID of. One of "master", * "self_signing", or "user_signing". Defaults to "master". * - * @return {string} the ID + * @returns the ID */ - - getId(type = "master") { if (!this.keys[type]) return null; const keyInfo = this.keys[type]; return publicKeyFromKeyInfo(keyInfo); } + /** * Create new cross-signing keys for the given key types. The public keys * will be held in this class, while the private keys are passed off to the * `saveCrossSigningKeys` application callback. * - * @param {CrossSigningLevel} level The key types to reset + * @param level - The key types to reset */ - - async resetKeys(level) { if (!this.callbacks.saveCrossSigningKeys) { throw new Error("No saveCrossSigningKeys callback supplied"); - } // If we're resetting the master key, we reset all keys - + } + // If we're resetting the master key, we reset all keys if (level === undefined || level & CrossSigningLevel.MASTER || !this.keys.master) { level = CrossSigningLevel.MASTER | CrossSigningLevel.USER_SIGNING | CrossSigningLevel.SELF_SIGNING; } else if (level === 0) { return; } - const privateKeys = {}; const keys = {}; let masterSigning; let masterPub; - try { if (level & CrossSigningLevel.MASTER) { masterSigning = new global.Olm.PkSigning(); @@ -301,26 +253,24 @@ masterPub = masterSigning.init_with_seed(privateKeys.master); keys.master = { user_id: this.userId, - usage: ['master'], + usage: ["master"], keys: { - ['ed25519:' + masterPub]: masterPub + ["ed25519:" + masterPub]: masterPub } }; } else { [masterPub, masterSigning] = await this.getCrossSigningKey("master"); } - if (level & CrossSigningLevel.SELF_SIGNING) { const sskSigning = new global.Olm.PkSigning(); - try { privateKeys.self_signing = sskSigning.generate_seed(); const sskPub = sskSigning.init_with_seed(privateKeys.self_signing); keys.self_signing = { user_id: this.userId, - usage: ['self_signing'], + usage: ["self_signing"], keys: { - ['ed25519:' + sskPub]: sskPub + ["ed25519:" + sskPub]: sskPub } }; (0, _olmlib.pkSign)(keys.self_signing, masterSigning, this.userId, masterPub); @@ -328,18 +278,16 @@ sskSigning.free(); } } - if (level & CrossSigningLevel.USER_SIGNING) { const uskSigning = new global.Olm.PkSigning(); - try { privateKeys.user_signing = uskSigning.generate_seed(); const uskPub = uskSigning.init_with_seed(privateKeys.user_signing); keys.user_signing = { user_id: this.userId, - usage: ['user_signing'], + usage: ["user_signing"], keys: { - ['ed25519:' + uskPub]: uskPub + ["ed25519:" + uskPub]: uskPub } }; (0, _olmlib.pkSign)(keys.user_signing, masterSigning, this.userId, masterPub); @@ -347,7 +295,6 @@ uskSigning.free(); } } - Object.assign(this.keys, keys); this.callbacks.saveCrossSigningKeys(privateKeys); } finally { @@ -356,27 +303,21 @@ } } } + /** * unsets the keys, used when another session has reset the keys, to disable cross-signing */ - - clearKeys() { this.keys = {}; } - setKeys(keys) { const signingKeys = {}; - if (keys.master) { if (keys.master.user_id !== this.userId) { const error = "Mismatched user ID " + keys.master.user_id + " in master key from " + this.userId; - _logger.logger.error(error); - throw new Error(error); } - if (!this.keys.master) { // this is the first key we've seen, so first-use is true this.firstUse = true; @@ -384,73 +325,58 @@ // this is a different key, so first-use is false this.firstUse = false; } // otherwise, same key, so no change - - signingKeys.master = keys.master; } else if (this.keys.master) { signingKeys.master = this.keys.master; } else { throw new Error("Tried to set cross-signing keys without a master key"); } + const masterKey = publicKeyFromKeyInfo(signingKeys.master); - const masterKey = publicKeyFromKeyInfo(signingKeys.master); // verify signatures - + // verify signatures if (keys.user_signing) { if (keys.user_signing.user_id !== this.userId) { const error = "Mismatched user ID " + keys.master.user_id + " in user_signing key from " + this.userId; - _logger.logger.error(error); - throw new Error(error); } - try { (0, _olmlib.pkVerify)(keys.user_signing, masterKey, this.userId); } catch (e) { - _logger.logger.error("invalid signature on user-signing key"); // FIXME: what do we want to do here? - - + _logger.logger.error("invalid signature on user-signing key"); + // FIXME: what do we want to do here? throw e; } } - if (keys.self_signing) { if (keys.self_signing.user_id !== this.userId) { const error = "Mismatched user ID " + keys.master.user_id + " in self_signing key from " + this.userId; - _logger.logger.error(error); - throw new Error(error); } - try { (0, _olmlib.pkVerify)(keys.self_signing, masterKey, this.userId); } catch (e) { - _logger.logger.error("invalid signature on self-signing key"); // FIXME: what do we want to do here? - - + _logger.logger.error("invalid signature on self-signing key"); + // FIXME: what do we want to do here? throw e; } - } // if everything checks out, then save the keys - + } + // if everything checks out, then save the keys if (keys.master) { - this.keys.master = keys.master; // if the master key is set, then the old self-signing and - // user-signing keys are obsolete - - this.keys.self_signing = null; - this.keys.user_signing = null; + this.keys.master = keys.master; + // if the master key is set, then the old self-signing and user-signing keys are obsolete + delete this.keys["self_signing"]; + delete this.keys["user_signing"]; } - if (keys.self_signing) { this.keys.self_signing = keys.self_signing; } - if (keys.user_signing) { this.keys.user_signing = keys.user_signing; } } - updateCrossSigningVerifiedBefore(isCrossSigningVerified) { // It is critical that this value latches forward from false to true but // never back to false to avoid a downgrade attack. @@ -458,14 +384,11 @@ this.crossSigningVerifiedBefore = true; } } - async signObject(data, type) { if (!this.keys[type]) { throw new Error("Attempted to sign with " + type + " key but no such key present"); } - const [pubkey, signing] = await this.getCrossSigningKey(type); - try { (0, _olmlib.pkSign)(data, signing, this.userId, pubkey); return data; @@ -473,28 +396,21 @@ signing.free(); } } - async signUser(key) { if (!this.keys.user_signing) { _logger.logger.info("No user signing key: not signing user"); - return; } - return this.signObject(key.keys.master, "user_signing"); } - async signDevice(userId, device) { if (userId !== this.userId) { throw new Error(`Trying to sign ${userId}'s device; can only sign our own device`); } - if (!this.keys.self_signing) { _logger.logger.info("No self signing key: not signing device"); - return; } - return this.signObject({ algorithms: device.algorithms, keys: device.keys, @@ -502,89 +418,76 @@ user_id: userId }, "self_signing"); } + /** * Check whether a given user is trusted. * - * @param {CrossSigningInfo} userCrossSigning Cross signing info for user + * @param userCrossSigning - Cross signing info for user * - * @returns {UserTrustLevel} + * @returns */ - - checkUserTrust(userCrossSigning) { // if we're checking our own key, then it's trusted if the master key // and self-signing key match if (this.userId === userCrossSigning.userId && this.getId() && this.getId() === userCrossSigning.getId() && this.getId("self_signing") && this.getId("self_signing") === userCrossSigning.getId("self_signing")) { return new UserTrustLevel(true, true, this.firstUse); } - if (!this.keys.user_signing) { // If there's no user signing key, they can't possibly be verified. // They may be TOFU trusted though. return new UserTrustLevel(false, false, userCrossSigning.firstUse); } - let userTrusted; const userMaster = userCrossSigning.keys.master; - const uskId = this.getId('user_signing'); - + const uskId = this.getId("user_signing"); try { (0, _olmlib.pkVerify)(userMaster, uskId, this.userId); userTrusted = true; } catch (e) { userTrusted = false; } - return new UserTrustLevel(userTrusted, userCrossSigning.crossSigningVerifiedBefore, userCrossSigning.firstUse); } + /** * Check whether a given device is trusted. * - * @param {CrossSigningInfo} userCrossSigning Cross signing info for user - * @param {module:crypto/deviceinfo} device The device to check - * @param {boolean} localTrust Whether the device is trusted locally - * @param {boolean} trustCrossSignedDevices Whether we trust cross signed devices + * @param userCrossSigning - Cross signing info for user + * @param device - The device to check + * @param localTrust - Whether the device is trusted locally + * @param trustCrossSignedDevices - Whether we trust cross signed devices * - * @returns {DeviceTrustLevel} + * @returns */ - - checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) { const userTrust = this.checkUserTrust(userCrossSigning); const userSSK = userCrossSigning.keys.self_signing; - if (!userSSK) { // if the user has no self-signing key then we cannot make any // trust assertions about this device from cross-signing return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices); } - const deviceObj = deviceToObject(device, userCrossSigning.userId); - try { // if we can verify the user's SSK from their master key... - (0, _olmlib.pkVerify)(userSSK, userCrossSigning.getId(), userCrossSigning.userId); // ...and this device's key from their SSK... - - (0, _olmlib.pkVerify)(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId); // ...then we trust this device as much as far as we trust the user - + (0, _olmlib.pkVerify)(userSSK, userCrossSigning.getId(), userCrossSigning.userId); + // ...and this device's key from their SSK... + (0, _olmlib.pkVerify)(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId); + // ...then we trust this device as much as far as we trust the user return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices); } catch (e) { return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices); } } + /** - * @returns {object} Cache callbacks + * @returns Cache callbacks */ - - getCacheCallbacks() { return this.cacheCallbacks; } - } - exports.CrossSigningInfo = CrossSigningInfo; - function deviceToObject(device, userId) { return { algorithms: device.algorithms, @@ -594,68 +497,57 @@ signatures: device.signatures }; } - let CrossSigningLevel; /** * Represents the ways in which we trust a user */ - exports.CrossSigningLevel = CrossSigningLevel; - (function (CrossSigningLevel) { CrossSigningLevel[CrossSigningLevel["MASTER"] = 4] = "MASTER"; CrossSigningLevel[CrossSigningLevel["USER_SIGNING"] = 2] = "USER_SIGNING"; CrossSigningLevel[CrossSigningLevel["SELF_SIGNING"] = 1] = "SELF_SIGNING"; })(CrossSigningLevel || (exports.CrossSigningLevel = CrossSigningLevel = {})); - class UserTrustLevel { constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) { this.crossSigningVerified = crossSigningVerified; this.crossSigningVerifiedBefore = crossSigningVerifiedBefore; this.tofu = tofu; } + /** - * @returns {boolean} true if this user is verified via any means + * @returns true if this user is verified via any means */ - - isVerified() { return this.isCrossSigningVerified(); } + /** - * @returns {boolean} true if this user is verified via cross signing + * @returns true if this user is verified via cross signing */ - - isCrossSigningVerified() { return this.crossSigningVerified; } + /** - * @returns {boolean} true if we ever verified this user before (at least for + * @returns true if we ever verified this user before (at least for * the history of verifications observed by this device). */ - - wasCrossSigningVerified() { return this.crossSigningVerifiedBefore; } + /** - * @returns {boolean} true if this user's key is trusted on first use + * @returns true if this user's key is trusted on first use */ - - isTofu() { return this.tofu; } - } + /** * Represents the ways in which we trust a device */ - - exports.UserTrustLevel = UserTrustLevel; - class DeviceTrustLevel { constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) { this.crossSigningVerified = crossSigningVerified; @@ -663,57 +555,48 @@ this.localVerified = localVerified; this.trustCrossSignedDevices = trustCrossSignedDevices; } - static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) { return new DeviceTrustLevel(userTrustLevel.isCrossSigningVerified(), userTrustLevel.isTofu(), localVerified, trustCrossSignedDevices); } + /** - * @returns {boolean} true if this device is verified via any means + * @returns true if this device is verified via any means */ - - isVerified() { return Boolean(this.isLocallyVerified() || this.trustCrossSignedDevices && this.isCrossSigningVerified()); } + /** - * @returns {boolean} true if this device is verified via cross signing + * @returns true if this device is verified via cross signing */ - - isCrossSigningVerified() { return this.crossSigningVerified; } + /** - * @returns {boolean} true if this device is verified locally + * @returns true if this device is verified locally */ - - isLocallyVerified() { return this.localVerified; } + /** - * @returns {boolean} true if this device is trusted from a user's key + * @returns true if this device is trusted from a user's key * that is trusted on first use */ - - isTofu() { return this.tofu; } - } - exports.DeviceTrustLevel = DeviceTrustLevel; - function createCryptoStoreCacheCallbacks(store, olmDevice) { return { getCrossSigningKeyCache: async function (type, _expectedPublicKey) { const key = await new Promise(resolve => { - return store.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + return store.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { store.getSecretStorePrivateKey(txn, resolve, type); }); }); - if (key && key.ciphertext) { const pickleKey = Buffer.from(olmDevice.pickleKey); const decrypted = await (0, _aes.decryptAES)(key, pickleKey, type); @@ -726,44 +609,40 @@ if (!(key instanceof Uint8Array)) { throw new Error(`storeCrossSigningKeyCache expects Uint8Array, got ${key}`); } - const pickleKey = Buffer.from(olmDevice.pickleKey); const encryptedKey = await (0, _aes.encryptAES)((0, _olmlib.encodeBase64)(key), pickleKey, type); - return store.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + return store.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { store.storeSecretStorePrivateKey(txn, type, encryptedKey); }); } }; } - /** * Request cross-signing keys from another device during verification. * - * @param {MatrixClient} baseApis base Matrix API interface - * @param {string} userId The user ID being verified - * @param {string} deviceId The device ID being verified + * @param baseApis - base Matrix API interface + * @param userId - The user ID being verified + * @param deviceId - The device ID being verified */ -function requestKeysDuringVerification(baseApis, userId, deviceId) { +async function requestKeysDuringVerification(baseApis, userId, deviceId) { // If this is a self-verification, ask the other party for keys if (baseApis.getUserId() !== userId) { return; } - - _logger.logger.log("Cross-signing: Self-verification done; requesting keys"); // This happens asynchronously, and we're not concerned about waiting for + _logger.logger.log("Cross-signing: Self-verification done; requesting keys"); + // This happens asynchronously, and we're not concerned about waiting for // it. We return here in order to test. - - return new Promise((resolve, reject) => { const client = baseApis; - const original = client.crypto.crossSigningInfo; // We already have all of the infrastructure we need to validate and + const original = client.crypto.crossSigningInfo; + + // We already have all of the infrastructure we need to validate and // cache cross-signing keys, so instead of replicating that, here we set // up callbacks that request them from the other device and call // CrossSigningInfo.getCrossSigningKey() to validate/cache - const crossSigning = new CrossSigningInfo(original.userId, { getCrossSigningKey: async type => { _logger.logger.debug("Cross-signing: requesting secret", type, deviceId); - const { promise } = client.requestSecret(`m.cross_signing.${type}`, [deviceId]); @@ -772,43 +651,37 @@ return Uint8Array.from(decoded); } }, original.getCacheCallbacks()); - crossSigning.keys = original.keys; // XXX: get all keys out if we get one key out + crossSigning.keys = original.keys; + + // XXX: get all keys out if we get one key out // https://github.com/vector-im/element-web/issues/12604 // then change here to reject on the timeout // Requests can be ignored, so don't wait around forever - const timeout = new Promise(resolve => { setTimeout(resolve, KEY_REQUEST_TIMEOUT_MS, new Error("Timeout")); - }); // also request and cache the key backup key + }); + // also request and cache the key backup key const backupKeyPromise = (async () => { const cachedKey = await client.crypto.getSessionBackupPrivateKey(); - if (!cachedKey) { _logger.logger.info("No cached backup key found. Requesting..."); - - const secretReq = client.requestSecret('m.megolm_backup.v1', [deviceId]); + const secretReq = client.requestSecret("m.megolm_backup.v1", [deviceId]); const base64Key = await secretReq.promise; - _logger.logger.info("Got key backup key, decoding..."); - const decodedKey = (0, _olmlib.decodeBase64)(base64Key); - _logger.logger.info("Decoded backup key, storing..."); - await client.crypto.storeSessionBackupPrivateKey(Uint8Array.from(decodedKey)); - _logger.logger.info("Backup key stored. Starting backup restore..."); - - const backupInfo = await client.getKeyBackupVersion(); // no need to await for this - just let it go in the bg - + const backupInfo = await client.getKeyBackupVersion(); + // no need to await for this - just let it go in the bg client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => { _logger.logger.info("Backup restored."); }); } - })(); // We call getCrossSigningKey() for its side-effects - + })(); + // We call getCrossSigningKey() for its side-effects return Promise.race([Promise.all([crossSigning.getCrossSigningKey("master"), crossSigning.getCrossSigningKey("self_signing"), crossSigning.getCrossSigningKey("user_signing"), backupKeyPromise]), timeout]).then(resolve, reject); }).catch(e => { _logger.logger.warn("Cross-signing: failure while requesting keys:", e); diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/crypto.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/crypto.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/crypto.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/crypto.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,60 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.crypto = exports.TextEncoder = void 0; +exports.setCrypto = setCrypto; +exports.setTextEncoder = setTextEncoder; +exports.subtleCrypto = void 0; +var _logger = require("../logger"); +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +let crypto = global.window?.crypto; +exports.crypto = crypto; +let subtleCrypto = global.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle; +exports.subtleCrypto = subtleCrypto; +let TextEncoder = global.window?.TextEncoder; + +/* eslint-disable @typescript-eslint/no-var-requires */ +exports.TextEncoder = TextEncoder; +if (!crypto) { + try { + exports.crypto = crypto = require("crypto").webcrypto; + } catch (e) { + _logger.logger.error("Failed to load webcrypto", e); + } +} +if (!subtleCrypto) { + exports.subtleCrypto = subtleCrypto = crypto?.subtle; +} +if (!TextEncoder) { + try { + exports.TextEncoder = TextEncoder = require("util").TextEncoder; + } catch (e) { + _logger.logger.error("Failed to load TextEncoder util", e); + } +} +/* eslint-enable @typescript-eslint/no-var-requires */ + +function setCrypto(_crypto) { + exports.crypto = crypto = _crypto; + exports.subtleCrypto = subtleCrypto = _crypto.subtle ?? _crypto.webkitSubtle; +} +function setTextEncoder(_TextEncoder) { + exports.TextEncoder = TextEncoder = _TextEncoder; +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/dehydration.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/dehydration.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/dehydration.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/dehydration.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,46 +4,31 @@ value: true }); exports.DehydrationManager = exports.DEHYDRATION_ALGORITHM = void 0; - var _anotherJson = _interopRequireDefault(require("another-json")); - var _olmlib = require("./olmlib"); - var _indexeddbCryptoStore = require("../crypto/store/indexeddb-crypto-store"); - var _aes = require("./aes"); - var _logger = require("../logger"); - var _httpApi = require("../http-api"); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle"; exports.DEHYDRATION_ALGORITHM = DEHYDRATION_ALGORITHM; const oneweek = 7 * 24 * 60 * 60 * 1000; - class DehydrationManager { constructor(crypto) { this.crypto = crypto; - _defineProperty(this, "inProgress", false); - _defineProperty(this, "timeoutId", void 0); - _defineProperty(this, "key", void 0); - _defineProperty(this, "keyInfo", void 0); - _defineProperty(this, "deviceDisplayName", void 0); - this.getDehydrationKeyFromCache(); } - getDehydrationKeyFromCache() { - return this.crypto.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + return this.crypto.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.crypto.cryptoStore.getSecretStorePrivateKey(txn, async result => { if (result) { const { @@ -64,77 +49,66 @@ }, "dehydration"); }); } - /** set the key, and queue periodic dehydration to the server in the background */ - - async setKeyAndQueueDehydration(key, keyInfo = {}, deviceDisplayName = undefined) { + /** set the key, and queue periodic dehydration to the server in the background */ + async setKeyAndQueueDehydration(key, keyInfo = {}, deviceDisplayName) { const matches = await this.setKey(key, keyInfo, deviceDisplayName); - if (!matches) { // start dehydration in the background this.dehydrateDevice(); } } - - async setKey(key, keyInfo = {}, deviceDisplayName = undefined) { + async setKey(key, keyInfo = {}, deviceDisplayName) { if (!key) { // unsetting the key -- cancel any pending dehydration task if (this.timeoutId) { global.clearTimeout(this.timeoutId); this.timeoutId = undefined; - } // clear storage - - - await this.crypto.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + } + // clear storage + await this.crypto.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", null); }); this.key = undefined; this.keyInfo = undefined; return; - } // Check to see if it's the same key as before. If it's different, + } + + // Check to see if it's the same key as before. If it's different, // dehydrate a new device. If it's the same, we can keep the same // device. (Assume that keyInfo and deviceDisplayName will be the // same if the key is the same.) - - - let matches = this.key && key.length == this.key.length; - + let matches = !!this.key && key.length == this.key.length; for (let i = 0; matches && i < key.length; i++) { if (key[i] != this.key[i]) { matches = false; } } - if (!matches) { this.key = key; this.keyInfo = keyInfo; this.deviceDisplayName = deviceDisplayName; } - return matches; } - /** returns the device id of the newly created dehydrated device */ - + /** returns the device id of the newly created dehydrated device */ async dehydrateDevice() { if (this.inProgress) { _logger.logger.log("Dehydration already in progress -- not starting new dehydration"); - return; } - this.inProgress = true; - if (this.timeoutId) { global.clearTimeout(this.timeoutId); this.timeoutId = undefined; } - try { - const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey); // update the crypto store with the timestamp + const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey); + // update the crypto store with the timestamp const key = await (0, _aes.encryptAES)((0, _olmlib.encodeBase64)(this.key), pickleKey, DEHYDRATION_ALGORITHM); - await this.crypto.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.crypto.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.crypto.cryptoStore.storeSecretStorePrivateKey(txn, "dehydration", { keyInfo: this.keyInfo, key, @@ -142,47 +116,41 @@ time: Date.now() }); }); - _logger.logger.log("Attempting to dehydrate device"); - - _logger.logger.log("Creating account"); // create the account and all the necessary keys - - + _logger.logger.log("Creating account"); + // create the account and all the necessary keys const account = new global.Olm.Account(); account.create(); const e2eKeys = JSON.parse(account.identity_keys()); - const maxKeys = account.max_number_of_one_time_keys(); // FIXME: generate in small batches? - + const maxKeys = account.max_number_of_one_time_keys(); + // FIXME: generate in small batches? account.generate_one_time_keys(maxKeys / 2); account.generate_fallback_key(); const otks = JSON.parse(account.one_time_keys()); const fallbacks = JSON.parse(account.fallback_key()); - account.mark_keys_as_published(); // dehydrate the account and store it on the server + account.mark_keys_as_published(); + // dehydrate the account and store it on the server const pickledAccount = account.pickle(new Uint8Array(this.key)); const deviceData = { algorithm: DEHYDRATION_ALGORITHM, account: pickledAccount }; - if (this.keyInfo.passphrase) { deviceData.passphrase = this.keyInfo.passphrase; } - - _logger.logger.log("Uploading account to server"); // eslint-disable-next-line camelcase - - - const dehydrateResult = await this.crypto.baseApis.http.authedRequest(undefined, _httpApi.Method.Put, "/dehydrated_device", undefined, { + _logger.logger.log("Uploading account to server"); + // eslint-disable-next-line camelcase + const dehydrateResult = await this.crypto.baseApis.http.authedRequest(_httpApi.Method.Put, "/dehydrated_device", undefined, { device_data: deviceData, initial_device_display_name: this.deviceDisplayName }, { prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2" - }); // send the keys to the server + }); + // send the keys to the server const deviceId = dehydrateResult.device_id; - _logger.logger.log("Preparing device keys", deviceId); - const deviceKeys = { algorithms: this.crypto.supportedAlgorithms, device_id: deviceId, @@ -198,15 +166,11 @@ [`ed25519:${deviceId}`]: deviceSignature } }; - if (this.crypto.crossSigningInfo.getId("self_signing")) { await this.crypto.crossSigningInfo.signObject(deviceKeys, "self_signing"); } - _logger.logger.log("Preparing one-time keys"); - const oneTimeKeys = {}; - for (const [keyId, key] of Object.entries(otks.curve25519)) { const k = { key @@ -219,11 +183,8 @@ }; oneTimeKeys[`signed_curve25519:${keyId}`] = k; } - _logger.logger.log("Preparing fallback keys"); - const fallbackKeys = {}; - for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { const k = { key, @@ -237,32 +198,26 @@ }; fallbackKeys[`signed_curve25519:${keyId}`] = k; } - _logger.logger.log("Uploading keys to server"); - - await this.crypto.baseApis.http.authedRequest(undefined, _httpApi.Method.Post, "/keys/upload/" + encodeURI(deviceId), undefined, { + await this.crypto.baseApis.http.authedRequest(_httpApi.Method.Post, "/keys/upload/" + encodeURI(deviceId), undefined, { "device_keys": deviceKeys, "one_time_keys": oneTimeKeys, "org.matrix.msc2732.fallback_keys": fallbackKeys }); + _logger.logger.log("Done dehydrating"); - _logger.logger.log("Done dehydrating"); // dehydrate again in a week - - + // dehydrate again in a week this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), oneweek); return deviceId; } finally { this.inProgress = false; } } - stop() { if (this.timeoutId) { global.clearTimeout(this.timeoutId); this.timeoutId = undefined; } } - } - exports.DehydrationManager = DehydrationManager; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/deviceinfo.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/deviceinfo.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/deviceinfo.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/deviceinfo.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,9 @@ value: true }); exports.DeviceInfo = void 0; - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. @@ -22,89 +22,52 @@ See the License for the specific language governing permissions and limitations under the License. */ - -/** - * @module crypto/deviceinfo - */ var DeviceVerification; /** - * Information about a user's device - * - * @constructor - * @alias module:crypto/deviceinfo - * - * @property {string} deviceId the ID of this device - * - * @property {string[]} algorithms list of algorithms supported by this device - * - * @property {Object.} keys a map from - * <key type>:<id> -> <base64-encoded key>> - * - * @property {module:crypto/deviceinfo.DeviceVerification} verified - * whether the device has been verified/blocked by the user - * - * @property {boolean} known - * whether the user knows of this device's existence (useful when warning - * the user that a user has added new devices) - * - * @property {Object} unsigned additional data from the homeserver - * - * @param {string} deviceId id of the device - */ - + * Information about a user's device + */ (function (DeviceVerification) { DeviceVerification[DeviceVerification["Blocked"] = -1] = "Blocked"; DeviceVerification[DeviceVerification["Unverified"] = 0] = "Unverified"; DeviceVerification[DeviceVerification["Verified"] = 1] = "Verified"; })(DeviceVerification || (DeviceVerification = {})); - class DeviceInfo { /** * rehydrate a DeviceInfo from the session store * - * @param {object} obj raw object from session store - * @param {string} deviceId id of the device + * @param obj - raw object from session store + * @param deviceId - id of the device * - * @return {module:crypto~DeviceInfo} new DeviceInfo + * @returns new DeviceInfo */ static fromStorage(obj, deviceId) { const res = new DeviceInfo(deviceId); - for (const prop in obj) { if (obj.hasOwnProperty(prop)) { + // @ts-ignore - this is messy and typescript doesn't like it res[prop] = obj[prop]; } } - return res; } /** - * @enum + * @param deviceId - id of the device */ - - constructor(deviceId) { this.deviceId = deviceId; - - _defineProperty(this, "algorithms", void 0); - + _defineProperty(this, "algorithms", []); _defineProperty(this, "keys", {}); - _defineProperty(this, "verified", DeviceVerification.Unverified); - _defineProperty(this, "known", false); - _defineProperty(this, "unsigned", {}); - _defineProperty(this, "signatures", {}); } + /** * Prepare a DeviceInfo for JSON serialisation in the session store * - * @return {object} deviceinfo with non-serialised members removed + * @returns deviceinfo with non-serialised members removed */ - - toStorage() { return { algorithms: this.algorithms, @@ -115,81 +78,71 @@ signatures: this.signatures }; } + /** * Get the fingerprint for this device (ie, the Ed25519 key) * - * @return {string} base64-encoded fingerprint of this device + * @returns base64-encoded fingerprint of this device */ - - getFingerprint() { return this.keys["ed25519:" + this.deviceId]; } + /** * Get the identity key for this device (ie, the Curve25519 key) * - * @return {string} base64-encoded identity key of this device + * @returns base64-encoded identity key of this device */ - - getIdentityKey() { return this.keys["curve25519:" + this.deviceId]; } + /** * Get the configured display name for this device, if any * - * @return {string?} displayname + * @returns displayname */ - - getDisplayName() { return this.unsigned.device_display_name || null; } + /** * Returns true if this device is blocked * - * @return {Boolean} true if blocked + * @returns true if blocked */ - - isBlocked() { return this.verified == DeviceVerification.Blocked; } + /** * Returns true if this device is verified * - * @return {Boolean} true if verified + * @returns true if verified */ - - isVerified() { return this.verified == DeviceVerification.Verified; } + /** * Returns true if this device is unverified * - * @return {Boolean} true if unverified + * @returns true if unverified */ - - isUnverified() { return this.verified == DeviceVerification.Unverified; } + /** * Returns true if the user knows about this device's existence * - * @return {Boolean} true if known + * @returns true if known */ - - isKnown() { return this.known === true; } - } - exports.DeviceInfo = DeviceInfo; - _defineProperty(DeviceInfo, "DeviceVerification", { VERIFIED: DeviceVerification.Verified, UNVERIFIED: DeviceVerification.Unverified, diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/DeviceList.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,29 +4,19 @@ value: true }); exports.TrackingStatus = exports.DeviceList = void 0; - var _logger = require("../logger"); - var _deviceinfo = require("./deviceinfo"); - var _CrossSigning = require("./CrossSigning"); - var olmlib = _interopRequireWildcard(require("./olmlib")); - var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store"); - var _utils = require("../utils"); - var _typedEventEmitter = require("../models/typed-event-emitter"); - var _index = require("./index"); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* State transition diagram for DeviceList.deviceTrackingStatus * * | @@ -47,88 +37,75 @@ * +----------------------- UP_TO_DATE ------------------------+ */ // constants for DeviceList.deviceTrackingStatus -let TrackingStatus; +let TrackingStatus; // user-Id → device-Id → DeviceInfo exports.TrackingStatus = TrackingStatus; - (function (TrackingStatus) { TrackingStatus[TrackingStatus["NotTracked"] = 0] = "NotTracked"; TrackingStatus[TrackingStatus["PendingDownload"] = 1] = "PendingDownload"; TrackingStatus[TrackingStatus["DownloadInProgress"] = 2] = "DownloadInProgress"; TrackingStatus[TrackingStatus["UpToDate"] = 3] = "UpToDate"; })(TrackingStatus || (exports.TrackingStatus = TrackingStatus = {})); - -/** - * @alias module:crypto/DeviceList - */ class DeviceList extends _typedEventEmitter.TypedEventEmitter { // map of identity keys to the user who owns it + // which users we are tracking device status for. // loaded from storage in load() + // The 'next_batch' sync token at the point the data was written, // ie. a token representing the point immediately after the // moment represented by the snapshot in the db. + // Set whenever changes are made other than setting the sync token + // Promise resolved when device data is saved + // Function that resolves the save promise + // The time the save is scheduled for + // The timer used to delay the save + // True if we have fetched data from the server or loaded a non-empty // set of device data from the store - constructor(baseApis, cryptoStore, olmDevice, // Maximum number of user IDs per request to prevent server overload (#1619) + + constructor(baseApis, cryptoStore, olmDevice, + // Maximum number of user IDs per request to prevent server overload (#1619) keyDownloadChunkSize = 250) { super(); this.cryptoStore = cryptoStore; this.keyDownloadChunkSize = keyDownloadChunkSize; - _defineProperty(this, "devices", {}); - _defineProperty(this, "crossSigningInfo", {}); - _defineProperty(this, "userByIdentityKey", {}); - _defineProperty(this, "deviceTrackingStatus", {}); - _defineProperty(this, "syncToken", null); - - _defineProperty(this, "keyDownloadsInProgressByUser", {}); - + _defineProperty(this, "keyDownloadsInProgressByUser", new Map()); _defineProperty(this, "dirty", false); - _defineProperty(this, "savePromise", null); - _defineProperty(this, "resolveSavePromise", null); - _defineProperty(this, "savePromiseTime", null); - _defineProperty(this, "saveTimer", null); - _defineProperty(this, "hasFetched", null); - _defineProperty(this, "serialiser", void 0); - this.serialiser = new DeviceListUpdateSerialiser(baseApis, olmDevice, this); } + /** * Load the device tracking state from storage */ - - async load() { - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_DEVICE_DATA], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_DEVICE_DATA], txn => { this.cryptoStore.getEndToEndDeviceData(txn, deviceData => { this.hasFetched = Boolean(deviceData && deviceData.devices); this.devices = deviceData ? deviceData.devices : {}; this.crossSigningInfo = deviceData ? deviceData.crossSigningInfo || {} : {}; this.deviceTrackingStatus = deviceData ? deviceData.trackingStatus : {}; - this.syncToken = deviceData ? deviceData.syncToken : null; + this.syncToken = deviceData?.syncToken ?? null; this.userByIdentityKey = {}; - for (const user of Object.keys(this.devices)) { const userDevices = this.devices[user]; - for (const device of Object.keys(userDevices)) { - const idKey = userDevices[device].keys['curve25519:' + device]; - + const idKey = userDevices[device].keys["curve25519:" + device]; if (idKey !== undefined) { this.userByIdentityKey[idKey] = user; } @@ -136,7 +113,6 @@ } }); }); - for (const u of Object.keys(this.deviceTrackingStatus)) { // if a download was in progress when we got shut down, it isn't any more. if (this.deviceTrackingStatus[u] == TrackingStatus.DownloadInProgress) { @@ -144,12 +120,12 @@ } } } - stop() { if (this.saveTimer !== null) { clearTimeout(this.saveTimer); } } + /** * Save the device tracking state to storage, if any changes are * pending other than updating the sync token @@ -157,85 +133,80 @@ * The actual save will be delayed by a short amount of time to * aggregate multiple writes to the database. * - * @param {number} delay Time in ms before which the save actually happens. + * @param delay - Time in ms before which the save actually happens. * By default, the save is delayed for a short period in order to batch * multiple writes, but this behaviour can be disabled by passing 0. * - * @return {Promise} true if the data was saved, false if + * @returns true if the data was saved, false if * it was not (eg. because no changes were pending). The promise * will only resolve once the data is saved, so may take some time * to resolve. */ - - async saveIfDirty(delay = 500) { - if (!this.dirty) return Promise.resolve(false); // Delay saves for a bit so we can aggregate multiple saves that happen + if (!this.dirty) return Promise.resolve(false); + // Delay saves for a bit so we can aggregate multiple saves that happen // in quick succession (eg. when a whole room's devices are marked as known) const targetTime = Date.now() + delay; - if (this.savePromiseTime && targetTime < this.savePromiseTime) { // There's a save scheduled but for after we would like: cancel // it & schedule one for the time we want clearTimeout(this.saveTimer); this.saveTimer = null; - this.savePromiseTime = null; // (but keep the save promise since whatever called save before + this.savePromiseTime = null; + // (but keep the save promise since whatever called save before // will still want to know when the save is done) } let savePromise = this.savePromise; - if (savePromise === null) { savePromise = new Promise(resolve => { this.resolveSavePromise = resolve; }); this.savePromise = savePromise; } - if (this.saveTimer === null) { const resolveSavePromise = this.resolveSavePromise; this.savePromiseTime = targetTime; this.saveTimer = setTimeout(() => { - _logger.logger.log('Saving device tracking data', this.syncToken); // null out savePromise now (after the delay but before the write), + _logger.logger.log("Saving device tracking data", this.syncToken); + + // null out savePromise now (after the delay but before the write), // otherwise we could return the existing promise when the save has // actually already happened. - - this.savePromiseTime = null; this.saveTimer = null; this.savePromise = null; this.resolveSavePromise = null; - this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_DEVICE_DATA], txn => { + this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_DEVICE_DATA], txn => { this.cryptoStore.storeEndToEndDeviceData({ devices: this.devices, crossSigningInfo: this.crossSigningInfo, trackingStatus: this.deviceTrackingStatus, - syncToken: this.syncToken + syncToken: this.syncToken ?? undefined }, txn); }).then(() => { // The device list is considered dirty until the write completes. this.dirty = false; - resolveSavePromise(true); + resolveSavePromise?.(true); }, err => { - _logger.logger.error('Failed to save device tracking data', this.syncToken); - + _logger.logger.error("Failed to save device tracking data", this.syncToken); _logger.logger.error(err); }); }, delay); } - return savePromise; } + /** * Gets the sync token last set with setSyncToken * - * @return {string} The sync token + * @returns The sync token */ - - getSyncToken() { return this.syncToken; } + /** * Sets the sync token that the app will pass as the 'since' to the /sync * endpoint next time it syncs. @@ -244,237 +215,198 @@ * those changed will not be synced from the server if a new client starts * up with that data. * - * @param {string} st The sync token + * @param st - The sync token */ - - setSyncToken(st) { this.syncToken = st; } + /** * Ensures up to date keys for a list of users are stored in the session store, * downloading and storing them if they're not (or if forceDownload is * true). - * @param {Array} userIds The users to fetch. - * @param {boolean} forceDownload Always download the keys even if cached. + * @param userIds - The users to fetch. + * @param forceDownload - Always download the keys even if cached. * - * @return {Promise} A promise which resolves to a map userId->deviceId->{@link - * module:crypto/deviceinfo|DeviceInfo}. + * @returns A promise which resolves to a map userId-\>deviceId-\>{@link DeviceInfo}. */ - - downloadKeys(userIds, forceDownload) { const usersToDownload = []; const promises = []; userIds.forEach(u => { const trackingStatus = this.deviceTrackingStatus[u]; - - if (this.keyDownloadsInProgressByUser[u]) { + if (this.keyDownloadsInProgressByUser.has(u)) { // already a key download in progress/queued for this user; its results // will be good enough for us. _logger.logger.log(`downloadKeys: already have a download in progress for ` + `${u}: awaiting its result`); - - promises.push(this.keyDownloadsInProgressByUser[u]); + promises.push(this.keyDownloadsInProgressByUser.get(u)); } else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) { usersToDownload.push(u); } }); - if (usersToDownload.length != 0) { _logger.logger.log("downloadKeys: downloading for", usersToDownload); - const downloadPromise = this.doKeyDownload(usersToDownload); promises.push(downloadPromise); } - if (promises.length === 0) { _logger.logger.log("downloadKeys: already have all necessary keys"); } - return Promise.all(promises).then(() => { return this.getDevicesFromStore(userIds); }); } + /** * Get the stored device keys for a list of user ids * - * @param {string[]} userIds the list of users to list keys for. + * @param userIds - the list of users to list keys for. * - * @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}. + * @returns userId-\>deviceId-\>{@link DeviceInfo}. */ - - getDevicesFromStore(userIds) { - const stored = {}; - userIds.forEach(u => { - stored[u] = {}; - const devices = this.getStoredDevicesForUser(u) || []; - devices.forEach(function (dev) { - stored[u][dev.deviceId] = dev; + const stored = new Map(); + userIds.forEach(userId => { + const deviceMap = new Map(); + this.getStoredDevicesForUser(userId)?.forEach(function (device) { + deviceMap.set(device.deviceId, device); }); + stored.set(userId, deviceMap); }); return stored; } + /** * Returns a list of all user IDs the DeviceList knows about * - * @return {array} All known user IDs + * @returns All known user IDs */ - - getKnownUserIds() { return Object.keys(this.devices); } + /** * Get the stored device keys for a user id * - * @param {string} userId the user to list keys for. + * @param userId - the user to list keys for. * - * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't + * @returns list of devices, or null if we haven't * managed to get a list of devices for this user yet. */ - - getStoredDevicesForUser(userId) { const devs = this.devices[userId]; - if (!devs) { return null; } - const res = []; - for (const deviceId in devs) { if (devs.hasOwnProperty(deviceId)) { res.push(_deviceinfo.DeviceInfo.fromStorage(devs[deviceId], deviceId)); } } - return res; } + /** * Get the stored device data for a user, in raw object form * - * @param {string} userId the user to get data for + * @param userId - the user to get data for * - * @return {Object} deviceId->{object} devices, or undefined if + * @returns `deviceId->{object}` devices, or undefined if * there is no data for this user. */ - - getRawStoredDevicesForUser(userId) { return this.devices[userId]; } - getStoredCrossSigningForUser(userId) { if (!this.crossSigningInfo[userId]) return null; return _CrossSigning.CrossSigningInfo.fromStorage(this.crossSigningInfo[userId], userId); } - storeCrossSigningForUser(userId, info) { this.crossSigningInfo[userId] = info; this.dirty = true; } + /** * Get the stored keys for a single device * - * @param {string} userId - * @param {string} deviceId * - * @return {module:crypto/deviceinfo?} device, or undefined + * @returns device, or undefined * if we don't know about this device */ - - getStoredDevice(userId, deviceId) { const devs = this.devices[userId]; - - if (!devs || !devs[deviceId]) { + if (!devs?.[deviceId]) { return undefined; } - return _deviceinfo.DeviceInfo.fromStorage(devs[deviceId], deviceId); } + /** * Get a user ID by one of their device's curve25519 identity key * - * @param {string} algorithm encryption algorithm - * @param {string} senderKey curve25519 key to match + * @param algorithm - encryption algorithm + * @param senderKey - curve25519 key to match * - * @return {string} user ID + * @returns user ID */ - - getUserByIdentityKey(algorithm, senderKey) { if (algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM) { // we only deal in olm keys return null; } - return this.userByIdentityKey[senderKey]; } + /** * Find a device by curve25519 identity key * - * @param {string} algorithm encryption algorithm - * @param {string} senderKey curve25519 key to match - * - * @return {module:crypto/deviceinfo?} + * @param algorithm - encryption algorithm + * @param senderKey - curve25519 key to match */ - - getDeviceByIdentityKey(algorithm, senderKey) { const userId = this.getUserByIdentityKey(algorithm, senderKey); - if (!userId) { return null; } - const devices = this.devices[userId]; - if (!devices) { return null; } - for (const deviceId in devices) { if (!devices.hasOwnProperty(deviceId)) { continue; } - const device = devices[deviceId]; - for (const keyId in device.keys) { if (!device.keys.hasOwnProperty(keyId)) { continue; } - if (keyId.indexOf("curve25519:") !== 0) { continue; } - const deviceKey = device.keys[keyId]; - if (deviceKey == senderKey) { return _deviceinfo.DeviceInfo.fromStorage(device, deviceId); } } - } // doesn't match a known device - + } + // doesn't match a known device return null; } + /** * Replaces the list of devices for a user with the given device list * - * @param {string} userId The user ID - * @param {Object} devices New device info for user + * @param userId - The user ID + * @param devices - New device info for user */ - - storeDevicesForUser(userId, devices) { this.setRawStoredDevicesForUser(userId, devices); this.dirty = true; } + /** * flag the given user for device-list tracking, if they are not already. * @@ -482,10 +414,7 @@ * will download the device list for the user, and that subsequent calls to * invalidateUserDeviceList will trigger more updates. * - * @param {String} userId */ - - startTrackingDeviceList(userId) { // sanity-check the userId. This is mostly paranoia, but if synapse // can't parse the userId we give it as an mxid, it 500s the whole @@ -494,19 +423,18 @@ // refresh request). // By checking it is at least a string, we can eliminate a class of // silly errors. - if (typeof userId !== 'string') { - throw new Error('userId must be a string; was ' + userId); + if (typeof userId !== "string") { + throw new Error("userId must be a string; was " + userId); } - if (!this.deviceTrackingStatus[userId]) { - _logger.logger.log('Now tracking device list for ' + userId); - - this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload; // we don't yet persist the tracking status, since there may be a lot + _logger.logger.log("Now tracking device list for " + userId); + this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload; + // we don't yet persist the tracking status, since there may be a lot // of calls; we save all data together once the sync is done - this.dirty = true; } } + /** * Mark the given user as no longer being tracked for device-list updates. * @@ -514,35 +442,31 @@ * complete; it will just mean that we don't think that we have an up-to-date * list for future calls to downloadKeys. * - * @param {String} userId */ - - stopTrackingDeviceList(userId) { if (this.deviceTrackingStatus[userId]) { - _logger.logger.log('No longer tracking device list for ' + userId); + _logger.logger.log("No longer tracking device list for " + userId); + this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked; - this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked; // we don't yet persist the tracking status, since there may be a lot + // we don't yet persist the tracking status, since there may be a lot // of calls; we save all data together once the sync is done - this.dirty = true; } } + /** * Set all users we're currently tracking to untracked * * This will flag each user whose devices we are tracking as in need of an * update. */ - - stopTrackingAllDeviceLists() { for (const userId of Object.keys(this.deviceTrackingStatus)) { this.deviceTrackingStatus[userId] = TrackingStatus.NotTracked; } - this.dirty = true; } + /** * Mark the cached device list for the given user outdated. * @@ -552,130 +476,113 @@ * This doesn't actually set off an update, so that several users can be * batched together. Call refreshOutdatedDeviceLists() for that. * - * @param {String} userId */ - - invalidateUserDeviceList(userId) { if (this.deviceTrackingStatus[userId]) { _logger.logger.log("Marking device list outdated for", userId); + this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload; - this.deviceTrackingStatus[userId] = TrackingStatus.PendingDownload; // we don't yet persist the tracking status, since there may be a lot + // we don't yet persist the tracking status, since there may be a lot // of calls; we save all data together once the sync is done - this.dirty = true; } } + /** * If we have users who have outdated device lists, start key downloads for them * - * @returns {Promise} which completes when the download completes; normally there + * @returns which completes when the download completes; normally there * is no need to wait for this (it's mostly for the unit tests). */ - - refreshOutdatedDeviceLists() { this.saveIfDirty(); const usersToDownload = []; - for (const userId of Object.keys(this.deviceTrackingStatus)) { const stat = this.deviceTrackingStatus[userId]; - if (stat == TrackingStatus.PendingDownload) { usersToDownload.push(userId); } } - return this.doKeyDownload(usersToDownload); } + /** * Set the stored device data for a user, in raw object form * Used only by internal class DeviceListUpdateSerialiser * - * @param {string} userId the user to get data for + * @param userId - the user to get data for * - * @param {Object} devices deviceId->{object} the new devices + * @param devices - `deviceId->{object}` the new devices */ - - setRawStoredDevicesForUser(userId, devices) { // remove old devices from userByIdentityKey if (this.devices[userId] !== undefined) { for (const [deviceId, dev] of Object.entries(this.devices[userId])) { - const identityKey = dev.keys['curve25519:' + deviceId]; + const identityKey = dev.keys["curve25519:" + deviceId]; delete this.userByIdentityKey[identityKey]; } } + this.devices[userId] = devices; - this.devices[userId] = devices; // add new devices into userByIdentityKey - + // add new devices into userByIdentityKey for (const [deviceId, dev] of Object.entries(devices)) { - const identityKey = dev.keys['curve25519:' + deviceId]; + const identityKey = dev.keys["curve25519:" + deviceId]; this.userByIdentityKey[identityKey] = userId; } } - setRawStoredCrossSigningForUser(userId, info) { this.crossSigningInfo[userId] = info; } + /** * Fire off download update requests for the given users, and update the * device list tracking status for them, and the * keyDownloadsInProgressByUser map for them. * - * @param {String[]} users list of userIds + * @param users - list of userIds * - * @return {Promise} resolves when all the users listed have + * @returns resolves when all the users listed have * been updated. rejects if there was a problem updating any of the * users. */ - - doKeyDownload(users) { if (users.length === 0) { // nothing to do return Promise.resolve(); } - const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken).then(() => { finished(true); }, e => { - _logger.logger.error('Error downloading keys for ' + users + ":", e); - + _logger.logger.error("Error downloading keys for " + users + ":", e); finished(false); throw e; }); users.forEach(u => { - this.keyDownloadsInProgressByUser[u] = prom; + this.keyDownloadsInProgressByUser.set(u, prom); const stat = this.deviceTrackingStatus[u]; - if (stat == TrackingStatus.PendingDownload) { this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress; } }); - const finished = success => { this.emit(_index.CryptoEvent.WillUpdateDevices, users, !this.hasFetched); users.forEach(u => { - this.dirty = true; // we may have queued up another download request for this user + this.dirty = true; + + // we may have queued up another download request for this user // since we started this request. If that happens, we should // ignore the completion of the first one. - - if (this.keyDownloadsInProgressByUser[u] !== prom) { - _logger.logger.log('Another update in the queue for', u, '- not marking up-to-date'); - + if (this.keyDownloadsInProgressByUser.get(u) !== prom) { + _logger.logger.log("Another update in the queue for", u, "- not marking up-to-date"); return; } - - delete this.keyDownloadsInProgressByUser[u]; + this.keyDownloadsInProgressByUser.delete(u); const stat = this.deviceTrackingStatus[u]; - if (stat == TrackingStatus.DownloadInProgress) { if (success) { // we didn't get any new invalidations since this download started: // this user's device list is now up to date. this.deviceTrackingStatus[u] = TrackingStatus.UpToDate; - _logger.logger.log("Device list for", u, "now up to date"); } else { this.deviceTrackingStatus[u] = TrackingStatus.PendingDownload; @@ -686,11 +593,10 @@ this.emit(_index.CryptoEvent.DevicesUpdated, users, !this.hasFetched); this.hasFetched = true; }; - return prom; } - } + /** * Serialises updates to device lists * @@ -700,119 +606,102 @@ * It currently does this by ensuring only one call to /keys/query happens at a * time (and queuing other requests up). */ - - exports.DeviceList = DeviceList; - class DeviceListUpdateSerialiser { // users which are queued for download // userId -> true + // deferred which is resolved when the queued users are downloaded. // non-null indicates that we have users queued for download. + // The sync token we send with the requests /* - * @param {object} baseApis Base API object - * @param {object} olmDevice The Olm Device - * @param {object} deviceList The device list object, the device list to be updated + * @param baseApis - Base API object + * @param olmDevice - The Olm Device + * @param deviceList - The device list object, the device list to be updated */ constructor(baseApis, olmDevice, deviceList) { this.baseApis = baseApis; this.olmDevice = olmDevice; this.deviceList = deviceList; - _defineProperty(this, "downloadInProgress", false); - _defineProperty(this, "keyDownloadsQueuedByUser", {}); - - _defineProperty(this, "queuedQueryDeferred", null); - - _defineProperty(this, "syncToken", null); + _defineProperty(this, "queuedQueryDeferred", void 0); + _defineProperty(this, "syncToken", void 0); } + /** * Make a key query request for the given users * - * @param {String[]} users list of user ids + * @param users - list of user ids * - * @param {String} syncToken sync token to pass in the query request, to + * @param syncToken - sync token to pass in the query request, to * help the HS give the most recent results * - * @return {Promise} resolves when all the users listed have + * @returns resolves when all the users listed have * been updated. rejects if there was a problem updating any of the * users. */ - - updateDevicesForUsers(users, syncToken) { users.forEach(u => { this.keyDownloadsQueuedByUser[u] = true; }); - if (!this.queuedQueryDeferred) { this.queuedQueryDeferred = (0, _utils.defer)(); - } // We always take the new sync token and just use the latest one we've + } + + // We always take the new sync token and just use the latest one we've // been given, since it just needs to be at least as recent as the // sync response the device invalidation message arrived in - - this.syncToken = syncToken; - if (this.downloadInProgress) { // just queue up these users - _logger.logger.log('Queued key download for', users); - + _logger.logger.log("Queued key download for", users); return this.queuedQueryDeferred.promise; - } // start a new download. - + } + // start a new download. return this.doQueuedQueries(); } - doQueuedQueries() { if (this.downloadInProgress) { throw new Error("DeviceListUpdateSerialiser.doQueuedQueries called with request active"); } - const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser); this.keyDownloadsQueuedByUser = {}; const deferred = this.queuedQueryDeferred; - this.queuedQueryDeferred = null; - - _logger.logger.log('Starting key download for', downloadUsers); - + this.queuedQueryDeferred = undefined; + _logger.logger.log("Starting key download for", downloadUsers); this.downloadInProgress = true; const opts = {}; - if (this.syncToken) { opts.token = this.syncToken; } - const factories = []; - for (let i = 0; i < downloadUsers.length; i += this.deviceList.keyDownloadChunkSize) { const userSlice = downloadUsers.slice(i, i + this.deviceList.keyDownloadChunkSize); factories.push(() => this.baseApis.downloadKeysForUsers(userSlice, opts)); } - (0, _utils.chunkPromises)(factories, 3).then(async responses => { const dk = Object.assign({}, ...responses.map(res => res.device_keys || {})); const masterKeys = Object.assign({}, ...responses.map(res => res.master_keys || {})); const ssks = Object.assign({}, ...responses.map(res => res.self_signing_keys || {})); - const usks = Object.assign({}, ...responses.map(res => res.user_signing_keys || {})); // yield to other things that want to execute in between users, to + const usks = Object.assign({}, ...responses.map(res => res.user_signing_keys || {})); + + // yield to other things that want to execute in between users, to // avoid wedging the CPU // (https://github.com/vector-im/element-web/issues/3158) // // of course we ought to do this in a web worker or similar, but // this serves as an easy solution for now. - for (const userId of downloadUsers) { await (0, _utils.sleep)(5); - try { await this.processQueryResponseForUser(userId, dk[userId], { - master: masterKeys[userId], - self_signing: ssks[userId], - user_signing: usks[userId] + master: masterKeys?.[userId], + self_signing: ssks?.[userId], + user_signing: usks?.[userId] }); } catch (e) { // log the error but continue, so that one bad key @@ -821,170 +710,144 @@ } } }).then(() => { - _logger.logger.log('Completed key download for ' + downloadUsers); - + _logger.logger.log("Completed key download for " + downloadUsers); this.downloadInProgress = false; - deferred.resolve(); // if we have queued users, fire off another request. + deferred?.resolve(); + // if we have queued users, fire off another request. if (this.queuedQueryDeferred) { this.doQueuedQueries(); } }, e => { - _logger.logger.warn('Error downloading keys for ' + downloadUsers + ':', e); - + _logger.logger.warn("Error downloading keys for " + downloadUsers + ":", e); this.downloadInProgress = false; - deferred.reject(e); + deferred?.reject(e); }); return deferred.promise; } - async processQueryResponseForUser(userId, dkResponse, crossSigningResponse) { - _logger.logger.log('got device keys for ' + userId + ':', dkResponse); - - _logger.logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse); - + _logger.logger.log("got device keys for " + userId + ":", dkResponse); + _logger.logger.log("got cross-signing keys for " + userId + ":", crossSigningResponse); { // map from deviceid -> deviceinfo for this user const userStore = {}; const devs = this.deviceList.getRawStoredDevicesForUser(userId); - if (devs) { Object.keys(devs).forEach(deviceId => { const d = _deviceinfo.DeviceInfo.fromStorage(devs[deviceId], deviceId); - userStore[deviceId] = d; }); } + await updateStoredDeviceKeysForUser(this.olmDevice, userId, userStore, dkResponse || {}, this.baseApis.getUserId(), this.baseApis.deviceId); - await updateStoredDeviceKeysForUser(this.olmDevice, userId, userStore, dkResponse || {}, this.baseApis.getUserId(), this.baseApis.deviceId); // put the updates into the object that will be returned as our results - + // put the updates into the object that will be returned as our results const storage = {}; Object.keys(userStore).forEach(deviceId => { storage[deviceId] = userStore[deviceId].toStorage(); }); this.deviceList.setRawStoredDevicesForUser(userId, storage); - } // now do the same for the cross-signing keys + } + // now do the same for the cross-signing keys { // FIXME: should we be ignoring empty cross-signing responses, or // should we be dropping the keys? if (crossSigningResponse && (crossSigningResponse.master || crossSigningResponse.self_signing || crossSigningResponse.user_signing)) { const crossSigning = this.deviceList.getStoredCrossSigningForUser(userId) || new _CrossSigning.CrossSigningInfo(userId); crossSigning.setKeys(crossSigningResponse); - this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage()); // NB. Unlike most events in the js-sdk, this one is internal to the - // js-sdk and is not re-emitted + this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage()); + // NB. Unlike most events in the js-sdk, this one is internal to the + // js-sdk and is not re-emitted this.deviceList.emit(_index.CryptoEvent.UserCrossSigningUpdated, userId); } } } - } - async function updateStoredDeviceKeysForUser(olmDevice, userId, userStore, userResult, localUserId, localDeviceId) { - let updated = false; // remove any devices in the store which aren't in the response + let updated = false; + // remove any devices in the store which aren't in the response for (const deviceId in userStore) { if (!userStore.hasOwnProperty(deviceId)) { continue; } - if (!(deviceId in userResult)) { if (userId === localUserId && deviceId === localDeviceId) { _logger.logger.warn(`Local device ${deviceId} missing from sync, skipping removal`); - continue; } - _logger.logger.log("Device " + userId + ":" + deviceId + " has been removed"); - delete userStore[deviceId]; updated = true; } } - for (const deviceId in userResult) { if (!userResult.hasOwnProperty(deviceId)) { continue; } + const deviceResult = userResult[deviceId]; - const deviceResult = userResult[deviceId]; // check that the user_id and device_id in the response object are + // check that the user_id and device_id in the response object are // correct - if (deviceResult.user_id !== userId) { _logger.logger.warn("Mismatched user_id " + deviceResult.user_id + " in keys from " + userId + ":" + deviceId); - continue; } - if (deviceResult.device_id !== deviceId) { _logger.logger.warn("Mismatched device_id " + deviceResult.device_id + " in keys from " + userId + ":" + deviceId); - continue; } - if (await storeDeviceKeys(olmDevice, userStore, deviceResult)) { updated = true; } } - return updated; } + /* * Process a device in a /query response, and add it to the userStore * * returns (a promise for) true if a change was made, else false */ - - async function storeDeviceKeys(olmDevice, userStore, deviceResult) { if (!deviceResult.keys) { // no keys? return false; } - const deviceId = deviceResult.device_id; const userId = deviceResult.user_id; const signKeyId = "ed25519:" + deviceId; const signKey = deviceResult.keys[signKeyId]; - if (!signKey) { _logger.logger.warn("Device " + userId + ":" + deviceId + " has no ed25519 key"); - return false; } - const unsigned = deviceResult.unsigned || {}; const signatures = deviceResult.signatures || {}; - try { await olmlib.verifySignature(olmDevice, deviceResult, userId, deviceId, signKey); } catch (e) { _logger.logger.warn("Unable to verify signature on device " + userId + ":" + deviceId + ":" + e); - return false; - } // DeviceInfo - + } + // DeviceInfo let deviceStore; - if (deviceId in userStore) { // already have this device. deviceStore = userStore[deviceId]; - if (deviceStore.getFingerprint() != signKey) { // this should only happen if the list has been MITMed; we are // best off sticking with the original keys. // // Should we warn the user about it somehow? _logger.logger.warn("Ed25519 key for device " + userId + ":" + deviceId + " has changed"); - return false; } } else { userStore[deviceId] = deviceStore = new _deviceinfo.DeviceInfo(deviceId); } - deviceStore.keys = deviceResult.keys || {}; deviceStore.algorithms = deviceResult.algorithms || []; deviceStore.unsigned = unsigned; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/EncryptionSetup.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/EncryptionSetup.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/EncryptionSetup.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/EncryptionSetup.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,23 +4,16 @@ value: true }); exports.EncryptionSetupOperation = exports.EncryptionSetupBuilder = void 0; - var _logger = require("../logger"); - var _event = require("../models/event"); - var _CrossSigning = require("./CrossSigning"); - var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store"); - var _httpApi = require("../http-api"); - -var _matrix = require("../matrix"); - +var _client = require("../client"); var _typedEventEmitter = require("../models/typed-event-emitter"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * Builds an EncryptionSetupOperation by calling any of the add.. methods. * Once done, `buildOperation()` can be called which allows to apply to operation. @@ -32,163 +25,123 @@ */ class EncryptionSetupBuilder { /** - * @param {Object.} accountData pre-existing account data, will only be read, not written. - * @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet + * @param accountData - pre-existing account data, will only be read, not written. + * @param delegateCryptoCallbacks - crypto callbacks to delegate to if the key isn't in cache yet */ constructor(accountData, delegateCryptoCallbacks) { _defineProperty(this, "accountDataClientAdapter", void 0); - _defineProperty(this, "crossSigningCallbacks", void 0); - _defineProperty(this, "ssssCryptoCallbacks", void 0); - - _defineProperty(this, "crossSigningKeys", null); - - _defineProperty(this, "keySignatures", null); - - _defineProperty(this, "keyBackupInfo", null); - + _defineProperty(this, "crossSigningKeys", void 0); + _defineProperty(this, "keySignatures", void 0); + _defineProperty(this, "keyBackupInfo", void 0); _defineProperty(this, "sessionBackupPrivateKey", void 0); - this.accountDataClientAdapter = new AccountDataClientAdapter(accountData); this.crossSigningCallbacks = new CrossSigningCallbacks(); this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks); } + /** * Adds new cross-signing public keys * - * @param {function} authUpload Function called to await an interactive auth + * @param authUpload - Function called to await an interactive auth * flow when uploading device signing keys. * Args: - * {function} A function that makes the request requiring auth. Receives + * A function that makes the request requiring auth. Receives * the auth data as an object. Can be called multiple times, first with * an empty authDict, to obtain the flows. - * @param {Object} keys the new keys + * @param keys - the new keys */ - - addCrossSigningKeys(authUpload, keys) { this.crossSigningKeys = { authUpload, keys }; } + /** * Adds the key backup info to be updated on the server * * Used either to create a new key backup, or add signatures * from the new MSK. * - * @param {Object} keyBackupInfo as received from/sent to the server + * @param keyBackupInfo - as received from/sent to the server */ - - addSessionBackup(keyBackupInfo) { this.keyBackupInfo = keyBackupInfo; } + /** * Adds the session backup private key to be updated in the local cache * * Used after fixing the format of the key * - * @param {Uint8Array} privateKey */ - - addSessionBackupPrivateKeyToCache(privateKey) { this.sessionBackupPrivateKey = privateKey; } + /** * Add signatures from a given user and device/x-sign key * Used to sign the new cross-signing key with the device key * - * @param {String} userId - * @param {String} deviceId - * @param {Object} signature */ - - addKeySignature(userId, deviceId, signature) { if (!this.keySignatures) { this.keySignatures = {}; } - const userSignatures = this.keySignatures[userId] || {}; this.keySignatures[userId] = userSignatures; userSignatures[deviceId] = signature; } - /** - * @param {String} type - * @param {Object} content - * @return {Promise} - */ - - async setAccountData(type, content) { await this.accountDataClientAdapter.setAccountData(type, content); } + /** * builds the operation containing all the parts that have been added to the builder - * @return {EncryptionSetupOperation} */ - - buildOperation() { const accountData = this.accountDataClientAdapter.values; return new EncryptionSetupOperation(accountData, this.crossSigningKeys, this.keyBackupInfo, this.keySignatures); } + /** * Stores the created keys locally. * * This does not yet store the operation in a way that it can be restored, * but that is the idea in the future. - * - * @param {Crypto} crypto - * @return {Promise} */ - - async persist(crypto) { // store private keys in cache if (this.crossSigningKeys) { const cacheCallbacks = (0, _CrossSigning.createCryptoStoreCacheCallbacks)(crypto.cryptoStore, crypto.olmDevice); - for (const type of ["master", "self_signing", "user_signing"]) { _logger.logger.log(`Cache ${type} cross-signing private key locally`); - const privateKey = this.crossSigningCallbacks.privateKeys.get(type); - await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey); - } // store own cross-sign pubkeys as trusted - - - await crypto.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await cacheCallbacks.storeCrossSigningKeyCache?.(type, privateKey); + } + // store own cross-sign pubkeys as trusted + await crypto.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { crypto.cryptoStore.storeCrossSigningKeys(txn, this.crossSigningKeys.keys); }); - } // store session backup key in cache - - + } + // store session backup key in cache if (this.sessionBackupPrivateKey) { await crypto.storeSessionBackupPrivateKey(this.sessionBackupPrivateKey); } } - } + /** * Can be created from EncryptionSetupBuilder, or * (in a follow-up PR, not implemented yet) restored from storage, to retry. * * It does not have knowledge of any private keys, unlike the builder. */ - - exports.EncryptionSetupBuilder = EncryptionSetupBuilder; - class EncryptionSetupOperation { /** - * @param {Map} accountData - * @param {Object} crossSigningKeys - * @param {Object} keyBackupInfo - * @param {Object} keySignatures */ constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) { this.accountData = accountData; @@ -196,221 +149,181 @@ this.keyBackupInfo = keyBackupInfo; this.keySignatures = keySignatures; } + /** * Runs the (remaining part of, in the future) operation by sending requests to the server. - * @param {Crypto} crypto */ - - async apply(crypto) { - const baseApis = crypto.baseApis; // upload cross-signing keys - + const baseApis = crypto.baseApis; + // upload cross-signing keys if (this.crossSigningKeys) { const keys = {}; - for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) { keys[name + "_key"] = key; - } // We must only call `uploadDeviceSigningKeys` from inside this auth - // helper to ensure we properly handle auth errors. - + } - await this.crossSigningKeys.authUpload(authDict => { + // We must only call `uploadDeviceSigningKeys` from inside this auth + // helper to ensure we properly handle auth errors. + await this.crossSigningKeys.authUpload?.(authDict => { return baseApis.uploadDeviceSigningKeys(authDict, keys); - }); // pass the new keys to the main instance of our own CrossSigningInfo. + }); + // pass the new keys to the main instance of our own CrossSigningInfo. crypto.crossSigningInfo.setKeys(this.crossSigningKeys.keys); - } // set account data - - + } + // set account data if (this.accountData) { for (const [type, content] of this.accountData) { await baseApis.setAccountData(type, content); } - } // upload first cross-signing signatures with the new key + } + // upload first cross-signing signatures with the new key // (e.g. signing our own device) - - if (this.keySignatures) { await baseApis.uploadKeySignatures(this.keySignatures); - } // need to create/update key backup info - - + } + // need to create/update key backup info if (this.keyBackupInfo) { if (this.keyBackupInfo.version) { // session backup signature // The backup is trusted because the user provided the private key. // Sign the backup with the cross signing key so the key backup can // be trusted via cross-signing. - await baseApis.http.authedRequest(undefined, _httpApi.Method.Put, "/room_keys/version/" + this.keyBackupInfo.version, undefined, { + await baseApis.http.authedRequest(_httpApi.Method.Put, "/room_keys/version/" + this.keyBackupInfo.version, undefined, { algorithm: this.keyBackupInfo.algorithm, auth_data: this.keyBackupInfo.auth_data }, { - prefix: _httpApi.PREFIX_UNSTABLE + prefix: _httpApi.ClientPrefix.V3 }); } else { // add new key backup - await baseApis.http.authedRequest(undefined, _httpApi.Method.Post, "/room_keys/version", undefined, this.keyBackupInfo, { - prefix: _httpApi.PREFIX_UNSTABLE + await baseApis.http.authedRequest(_httpApi.Method.Post, "/room_keys/version", undefined, this.keyBackupInfo, { + prefix: _httpApi.ClientPrefix.V3 }); } } } - } + /** * Catches account data set by SecretStorage during bootstrapping by * implementing the methods related to account data in MatrixClient */ - - exports.EncryptionSetupOperation = EncryptionSetupOperation; - class AccountDataClientAdapter extends _typedEventEmitter.TypedEventEmitter { // /** - * @param {Object.} existingValues existing account data + * @param existingValues - existing account data */ constructor(existingValues) { super(); this.existingValues = existingValues; - _defineProperty(this, "values", new Map()); } + /** - * @param {String} type - * @return {Promise} the content of the account data + * @returns the content of the account data */ - - getAccountDataFromServer(type) { return Promise.resolve(this.getAccountData(type)); } + /** - * @param {String} type - * @return {Object} the content of the account data + * @returns the content of the account data */ - - getAccountData(type) { const modifiedValue = this.values.get(type); - if (modifiedValue) { return modifiedValue; } - - const existingValue = this.existingValues[type]; - + const existingValue = this.existingValues.get(type); if (existingValue) { return existingValue.getContent(); } - return null; } - /** - * @param {String} type - * @param {Object} content - * @return {Promise} - */ - - setAccountData(type, content) { const lastEvent = this.values.get(type); - this.values.set(type, content); // ensure accountData is emitted on the next tick, + this.values.set(type, content); + // ensure accountData is emitted on the next tick, // as SecretStorage listens for it while calling this method // and it seems to rely on this. - return Promise.resolve().then(() => { const event = new _event.MatrixEvent({ type, content }); - this.emit(_matrix.ClientEvent.AccountData, event, lastEvent); + this.emit(_client.ClientEvent.AccountData, event, lastEvent); return {}; }); } - } + /** * Catches the private cross-signing keys set during bootstrapping * by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks. * See CrossSigningInfo constructor */ - - class CrossSigningCallbacks { constructor() { _defineProperty(this, "privateKeys", new Map()); } - // cache callbacks getCrossSigningKeyCache(type, expectedPublicKey) { return this.getCrossSigningKey(type, expectedPublicKey); } - storeCrossSigningKeyCache(type, key) { this.privateKeys.set(type, key); return Promise.resolve(); - } // non-cache callbacks - + } + // non-cache callbacks getCrossSigningKey(type, expectedPubkey) { - return Promise.resolve(this.privateKeys.get(type)); + return Promise.resolve(this.privateKeys.get(type) ?? null); } - saveCrossSigningKeys(privateKeys) { for (const [type, privateKey] of Object.entries(privateKeys)) { this.privateKeys.set(type, privateKey); } } - } + /** * Catches the 4S private key set during bootstrapping by implementing * the SecretStorage crypto callbacks */ - - class SSSSCryptoCallbacks { constructor(delegateCryptoCallbacks) { this.delegateCryptoCallbacks = delegateCryptoCallbacks; - _defineProperty(this, "privateKeys", new Map()); } - async getSecretStorageKey({ keys }, name) { for (const keyId of Object.keys(keys)) { const privateKey = this.privateKeys.get(keyId); - if (privateKey) { return [keyId, privateKey]; } - } // if we don't have the key cached yet, ask + } + // if we don't have the key cached yet, ask // for it to the general crypto callbacks and cache it - - if (this?.delegateCryptoCallbacks?.getSecretStorageKey) { const result = await this.delegateCryptoCallbacks.getSecretStorageKey({ keys }, name); - if (result) { const [keyId, privateKey] = result; this.privateKeys.set(keyId, privateKey); } - return result; } - return null; } - addPrivateKey(keyId, keyInfo, privKey) { - this.privateKeys.set(keyId, privKey); // Also pass along to application to cache if it wishes - + this.privateKeys.set(keyId, privKey); + // Also pass along to application to cache if it wishes this.delegateCryptoCallbacks?.cacheSecretStorageKey?.(keyId, keyInfo, privKey); } - } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/index.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -7,81 +7,48 @@ exports.fixBackupKey = fixBackupKey; exports.isCryptoAvailable = isCryptoAvailable; exports.verificationMethods = void 0; - var _anotherJson = _interopRequireDefault(require("another-json")); - +var _uuid = require("uuid"); var _event = require("../@types/event"); - var _ReEmitter = require("../ReEmitter"); - var _logger = require("../logger"); - var _OlmDevice = require("./OlmDevice"); - var olmlib = _interopRequireWildcard(require("./olmlib")); - var _DeviceList = require("./DeviceList"); - var _deviceinfo = require("./deviceinfo"); - var algorithms = _interopRequireWildcard(require("./algorithms")); - var _CrossSigning = require("./CrossSigning"); - var _EncryptionSetup = require("./EncryptionSetup"); - var _SecretStorage = require("./SecretStorage"); - var _OutgoingRoomKeyRequestManager = require("./OutgoingRoomKeyRequestManager"); - var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store"); - var _QRCode = require("./verification/QRCode"); - var _SAS = require("./verification/SAS"); - var _key_passphrase = require("./key_passphrase"); - var _recoverykey = require("./recoverykey"); - var _VerificationRequest = require("./verification/request/VerificationRequest"); - var _InRoomChannel = require("./verification/request/InRoomChannel"); - var _ToDeviceChannel = require("./verification/request/ToDeviceChannel"); - var _IllegalMethod = require("./verification/IllegalMethod"); - var _errors = require("../errors"); - var _aes = require("./aes"); - var _dehydration = require("./dehydration"); - var _backup = require("./backup"); - var _room = require("../models/room"); - var _roomMember = require("../models/room-member"); - var _event2 = require("../models/event"); - var _client = require("../client"); - var _typedEventEmitter = require("../models/typed-event-emitter"); - +var _roomState = require("../models/room-state"); +var _utils = require("../utils"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const DeviceVerification = _deviceinfo.DeviceInfo.DeviceVerification; const defaultVerificationMethods = { [_QRCode.ReciprocateQRCode.NAME]: _QRCode.ReciprocateQRCode, @@ -92,27 +59,22 @@ [_QRCode.SHOW_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod, [_QRCode.SCAN_QR_CODE_METHOD]: _IllegalMethod.IllegalMethod }; + /** * verification method names */ // legacy export identifier - const verificationMethods = { RECIPROCATE_QR_CODE: _QRCode.ReciprocateQRCode.NAME, SAS: _SAS.SAS.NAME }; exports.verificationMethods = verificationMethods; - function isCryptoAvailable() { return Boolean(global.Olm); } - const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; - -/* eslint-enable camelcase */ let CryptoEvent; exports.CryptoEvent = CryptoEvent; - (function (CryptoEvent) { CryptoEvent["DeviceVerificationChanged"] = "deviceVerificationChanged"; CryptoEvent["UserTrustStatusChanged"] = "userTrustStatusChanged"; @@ -129,39 +91,33 @@ CryptoEvent["DevicesUpdated"] = "crypto.devicesUpdated"; CryptoEvent["KeysChanged"] = "crossSigning.keysChanged"; })(CryptoEvent || (exports.CryptoEvent = CryptoEvent = {})); - class Crypto extends _typedEventEmitter.TypedEventEmitter { /** - * @return {string} The version of Olm. + * @returns The version of Olm. */ static getOlmVersion() { return _OlmDevice.OlmDevice.getOlmVersion(); } - /** * Cryptography bits * * This module is internal to the js-sdk; the public API is via MatrixClient. * - * @constructor - * @alias module:crypto - * * @internal * - * @param {MatrixClient} baseApis base matrix api interface + * @param baseApis - base matrix api interface * - * @param {string} userId The user ID for the local user + * @param userId - The user ID for the local user * - * @param {string} deviceId The identifier for this device. + * @param deviceId - The identifier for this device. * - * @param {Object} clientStore the MatrixClient data store. + * @param clientStore - the MatrixClient data store. * - * @param {module:crypto/store/base~CryptoStore} cryptoStore - * storage for the crypto layer. + * @param cryptoStore - storage for the crypto layer. * - * @param {RoomList} roomList An initialised RoomList object + * @param roomList - An initialised RoomList object * - * @param {Array} verificationMethods Array of verification methods to use. + * @param verificationMethods - Array of verification methods to use. * Each element can either be a string from MatrixClient.verificationMethods * or a class that implements a verification method. */ @@ -173,67 +129,36 @@ this.clientStore = clientStore; this.cryptoStore = cryptoStore; this.roomList = roomList; - _defineProperty(this, "backupManager", void 0); - _defineProperty(this, "crossSigningInfo", void 0); - _defineProperty(this, "olmDevice", void 0); - _defineProperty(this, "deviceList", void 0); - _defineProperty(this, "dehydrationManager", void 0); - _defineProperty(this, "secretStorage", void 0); - _defineProperty(this, "reEmitter", void 0); - _defineProperty(this, "verificationMethods", void 0); - _defineProperty(this, "supportedAlgorithms", void 0); - _defineProperty(this, "outgoingRoomKeyRequestManager", void 0); - _defineProperty(this, "toDeviceVerificationRequests", void 0); - _defineProperty(this, "inRoomVerificationRequests", void 0); - _defineProperty(this, "trustCrossSignedDevices", true); - _defineProperty(this, "lastOneTimeKeyCheck", null); - _defineProperty(this, "oneTimeKeyCheckInProgress", false); - _defineProperty(this, "roomEncryptors", new Map()); - _defineProperty(this, "roomDecryptors", new Map()); - _defineProperty(this, "deviceKeys", {}); - _defineProperty(this, "globalBlacklistUnverifiedDevices", false); - _defineProperty(this, "globalErrorOnUnknownDevices", true); - _defineProperty(this, "receivedRoomKeyRequests", []); - _defineProperty(this, "receivedRoomKeyRequestCancellations", []); - _defineProperty(this, "processingRoomKeyRequests", false); - _defineProperty(this, "lazyLoadMembers", false); - _defineProperty(this, "roomDeviceTrackingState", {}); - - _defineProperty(this, "lastNewSessionForced", {}); - + _defineProperty(this, "lastNewSessionForced", new _utils.MapWithDefault(() => new _utils.MapWithDefault(() => 0))); _defineProperty(this, "sendKeyRequestsImmediately", false); - _defineProperty(this, "oneTimeKeyCount", void 0); - _defineProperty(this, "needsNewFallback", void 0); - _defineProperty(this, "fallbackCleanup", void 0); - _defineProperty(this, "onDeviceListUserCrossSigningUpdated", async userId => { if (userId === this.userId) { // An update to our own cross-signing key. @@ -242,7 +167,6 @@ const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null; const currentPubkey = this.crossSigningInfo.getId(); const changed = currentPubkey !== seenPubkey; - if (currentPubkey && seenPubkey && !changed) { // If it's not changed, just make sure everything is up to date await this.checkOwnCrossSigningTrust(); @@ -252,28 +176,26 @@ // on the server for our account. So we clear our own stored cross-signing keys, // effectively disabling cross-signing until the user gets verified by the device // that reset the keys - this.storeTrustedSelfKeys(null); // emit cross-signing has been disabled - - this.emit(CryptoEvent.KeysChanged, {}); // as the trust for our own user has changed, + this.storeTrustedSelfKeys(null); + // emit cross-signing has been disabled + this.emit(CryptoEvent.KeysChanged, {}); + // as the trust for our own user has changed, // also emit an event for this - this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId)); } } else { - await this.checkDeviceVerifications(userId); // Update verified before latch using the current state and save the new - // latch value in the device list store. + await this.checkDeviceVerifications(userId); + // Update verified before latch using the current state and save the new + // latch value in the device list store. const crossSigning = this.deviceList.getStoredCrossSigningForUser(userId); - if (crossSigning) { crossSigning.updateCrossSigningVerifiedBefore(this.checkUserTrust(userId).isCrossSigningVerified()); this.deviceList.setRawStoredCrossSigningForUser(userId, crossSigning.toStorage()); } - this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId)); } }); - _defineProperty(this, "onMembership", (event, member, oldMembership) => { try { this.onRoomMembership(event, member, oldMembership); @@ -281,11 +203,9 @@ _logger.logger.error("Error handling membership change:", e); } }); - _defineProperty(this, "onToDeviceEvent", event => { try { - _logger.logger.log(`received to_device ${event.getType()} from: ` + `${event.getSender()} id: ${event.getId()}`); - + _logger.logger.log(`received to-device ${event.getType()} from: ` + `${event.getSender()} id: ${event.getContent()[_event.ToDeviceMessageId]}`); if (event.getType() == "m.room_key" || event.getType() == "m.forwarded_room_key") { this.onRoomKeyEvent(event); } else if (event.getType() == "m.room_key_request") { @@ -303,9 +223,8 @@ } else if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { if (!event.isBeingDecrypted()) { event.attemptDecryption(this); - } // once the event has been decrypted, try again - - + } + // once the event has been decrypted, try again event.once(_event2.MatrixEventEvent.Decrypted, ev => { this.onToDeviceEvent(ev); }); @@ -314,27 +233,21 @@ _logger.logger.error("Error handling toDeviceEvent:", e); } }); - _defineProperty(this, "onTimelineEvent", (event, room, atStart, removed, { liveEvent = true } = {}) => { if (!_InRoomChannel.InRoomChannel.validateEvent(event, this.baseApis)) { return; } - const createRequest = event => { const channel = new _InRoomChannel.InRoomChannel(this.baseApis, event.getRoomId()); return new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis); }; - this.handleVerificationEvent(event, this.inRoomVerificationRequests, createRequest, liveEvent); }); - this.reEmitter = new _ReEmitter.TypedReEmitter(this); - if (verificationMethods) { this.verificationMethods = new Map(); - for (const method of verificationMethods) { if (typeof method === "string") { if (defaultVerificationMethods[method]) { @@ -349,42 +262,37 @@ } else { this.verificationMethods = new Map(Object.entries(defaultVerificationMethods)); } - this.backupManager = new _backup.BackupManager(baseApis, async () => { // try to get key from cache const cachedKey = await this.getSessionBackupPrivateKey(); - if (cachedKey) { return cachedKey; - } // try to get key from secret storage - + } + // try to get key from secret storage const storedKey = await this.getSecret("m.megolm_backup.v1"); - if (storedKey) { // ensure that the key is in the right format. If not, fix the key and // store the fixed version const fixedKey = fixBackupKey(storedKey); - if (fixedKey) { - const [keyId] = await this.getSecretStorageKey(); - await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); + const keys = await this.getSecretStorageKey(); + await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys[0]]); } - return olmlib.decodeBase64(fixedKey || storedKey); - } // try to get key from app - + } + // try to get key from app if (this.baseApis.cryptoCallbacks && this.baseApis.cryptoCallbacks.getBackupKey) { return this.baseApis.cryptoCallbacks.getBackupKey(); } - throw new Error("Unable to get private key"); }); this.olmDevice = new _OlmDevice.OlmDevice(cryptoStore); - this.deviceList = new _DeviceList.DeviceList(baseApis, cryptoStore, this.olmDevice); // XXX: This isn't removed at any point, but then none of the event listeners - // this class sets seem to be removed at any point... :/ + this.deviceList = new _DeviceList.DeviceList(baseApis, cryptoStore, this.olmDevice); + // XXX: This isn't removed at any point, but then none of the event listeners + // this class sets seem to be removed at any point... :/ this.deviceList.on(CryptoEvent.UserCrossSigningUpdated, this.onDeviceListUserCrossSigningUpdated); this.reEmitter.reEmit(this.deviceList, [CryptoEvent.DevicesUpdated, CryptoEvent.WillUpdateDevices]); this.supportedAlgorithms = Array.from(algorithms.DECRYPTION_CLASSES.keys()); @@ -393,62 +301,52 @@ this.inRoomVerificationRequests = new _InRoomChannel.InRoomRequests(); const cryptoCallbacks = this.baseApis.cryptoCallbacks || {}; const cacheCallbacks = (0, _CrossSigning.createCryptoStoreCacheCallbacks)(cryptoStore, this.olmDevice); - this.crossSigningInfo = new _CrossSigning.CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks); // Yes, we pass the client twice here: see SecretStorage - + this.crossSigningInfo = new _CrossSigning.CrossSigningInfo(userId, cryptoCallbacks, cacheCallbacks); + // Yes, we pass the client twice here: see SecretStorage this.secretStorage = new _SecretStorage.SecretStorage(baseApis, cryptoCallbacks, baseApis); - this.dehydrationManager = new _dehydration.DehydrationManager(this); // Assuming no app-supplied callback, default to getting from SSSS. + this.dehydrationManager = new _dehydration.DehydrationManager(this); + // Assuming no app-supplied callback, default to getting from SSSS. if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) { cryptoCallbacks.getCrossSigningKey = async type => { return _CrossSigning.CrossSigningInfo.getFromSecretStorage(type, this.secretStorage); }; } } + /** * Initialise the crypto module so that it is ready for use * * Returns a promise which resolves once the crypto module is ready for use. * - * @param {Object} opts keyword arguments. - * @param {string} opts.exportedOlmDevice (Optional) data from exported device + * @param exportedOlmDevice - (Optional) data from exported device * that must be re-created. */ - - async init({ exportedOlmDevice, pickleKey } = {}) { _logger.logger.log("Crypto: initialising Olm..."); - await global.Olm.init(); - _logger.logger.log(exportedOlmDevice ? "Crypto: initialising Olm device from exported device..." : "Crypto: initialising Olm device..."); - await this.olmDevice.init({ fromExportedDevice: exportedOlmDevice, pickleKey }); - _logger.logger.log("Crypto: loading device list..."); + await this.deviceList.load(); - await this.deviceList.load(); // build our device keys: these will later be uploaded - + // build our device keys: these will later be uploaded this.deviceKeys["ed25519:" + this.deviceId] = this.olmDevice.deviceEd25519Key; this.deviceKeys["curve25519:" + this.deviceId] = this.olmDevice.deviceCurve25519Key; - _logger.logger.log("Crypto: fetching own devices..."); - let myDevices = this.deviceList.getRawStoredDevicesForUser(this.userId); - if (!myDevices) { myDevices = {}; } - if (!myDevices[this.deviceId]) { // add our own deviceinfo to the cryptoStore _logger.logger.log("Crypto: adding this device to the store..."); - const deviceInfo = { keys: this.deviceKeys, algorithms: this.supportedAlgorithms, @@ -459,25 +357,22 @@ this.deviceList.storeDevicesForUser(this.userId, myDevices); this.deviceList.saveIfDirty(); } - - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.cryptoStore.getCrossSigningKeys(txn, keys => { // can be an empty object after resetting cross-signing keys, see storeTrustedSelfKeys if (keys && Object.keys(keys).length !== 0) { _logger.logger.log("Loaded cross-signing public keys from crypto store"); - this.crossSigningInfo.setKeys(keys); } }); - }); // make sure we are keeping track of our own devices + }); + // make sure we are keeping track of our own devices // (this is important for key backups & things) - this.deviceList.startTrackingDeviceList(this.userId); - _logger.logger.log("Crypto: checking for key backup..."); - this.backupManager.checkAndStart(); } + /** * Whether to trust a others users signatures of their devices. * If false, devices will only be considered 'verified' if we have @@ -485,32 +380,27 @@ * * Default: true * - * @return {boolean} True if trusting cross-signed devices + * @returns True if trusting cross-signed devices */ - - getCryptoTrustCrossSignedDevices() { return this.trustCrossSignedDevices; } + /** * See getCryptoTrustCrossSignedDevices * This may be set before initCrypto() is called to ensure no races occur. * - * @param {boolean} val True to trust cross-signed devices + * @param val - True to trust cross-signed devices */ - - setCryptoTrustCrossSignedDevices(val) { this.trustCrossSignedDevices = val; - for (const userId of this.deviceList.getKnownUserIds()) { const devices = this.deviceList.getRawStoredDevicesForUser(userId); - for (const deviceId of Object.keys(devices)) { - const deviceTrust = this.checkDeviceTrust(userId, deviceId); // If the device is locally verified then isVerified() is always true, + const deviceTrust = this.checkDeviceTrust(userId, deviceId); + // If the device is locally verified then isVerified() is always true, // so this will only have caused the value to change if the device is // cross-signing verified but not locally verified - if (!deviceTrust.isLocallyVerified() && deviceTrust.isCrossSigningVerified()) { const deviceObj = this.deviceList.getStoredDevice(userId, deviceId); this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj); @@ -518,24 +408,21 @@ } } } + /** * Create a recovery key from a user-supplied passphrase. * - * @param {string} password Passphrase string that can be entered by the user + * @param password - Passphrase string that can be entered by the user * when restoring the backup as an alternative to entering the recovery key. * Optional. - * @returns {Promise} Object with public key metadata, encoded private + * @returns Object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. */ - - async createRecoveryKeyFromPassphrase(password) { const decryption = new global.Olm.PkDecryption(); - try { const keyInfo = {}; - if (password) { const derivation = await (0, _key_passphrase.keyFromPassphrase)(password); keyInfo.passphrase = { @@ -547,7 +434,6 @@ } else { keyInfo.pubkey = decryption.generate_key(); } - const privateKey = decryption.get_private_key(); const encodedPrivateKey = (0, _recoverykey.encodeRecoveryKey)(privateKey); return { @@ -556,9 +442,23 @@ privateKey }; } finally { - if (decryption) decryption.free(); + decryption?.free(); } } + + /** + * Checks if the user has previously published cross-signing keys + * + * This means downloading the devicelist for the user and checking if the list includes + * the cross-signing pseudo-device. + * + * @internal + */ + async userHasCrossSigningKeys() { + await this.downloadKeys([this.userId]); + return this.deviceList.getStoredCrossSigningForUser(this.userId) !== null; + } + /** * Checks whether cross signing: * - is enabled on this account and trusted by this device @@ -571,15 +471,14 @@ * * The cross-signing API is currently UNSTABLE and may change without notice. * - * @return {boolean} True if cross-signing is ready to be used on this device + * @returns True if cross-signing is ready to be used on this device */ - - async isCrossSigningReady() { const publicKeysOnDevice = this.crossSigningInfo.getId(); const privateKeysExistSomewhere = (await this.crossSigningInfo.isStoredInKeyCache()) || (await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage)); return !!(publicKeysOnDevice && privateKeysExistSomewhere); } + /** * Checks whether secret storage: * - is enabled on this account @@ -593,16 +492,15 @@ * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @return {boolean} True if secret storage is ready to be used on this device + * @returns True if secret storage is ready to be used on this device */ - - async isSecretStorageReady() { const secretStorageKeyInAccount = await this.secretStorage.hasKey(); const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage); const sessionBackupInStorage = !this.backupManager.getKeyBackupEnabled() || (await this.baseApis.isKeyBackupKeyStored()); return !!(secretStorageKeyInAccount && privateKeysInStorage && sessionBackupInStorage); } + /** * Bootstrap cross-signing by creating keys if needed. If everything is already * set up, then no changes are made, so this is safe to run to ensure @@ -614,50 +512,51 @@ * * The cross-signing API is currently UNSTABLE and may change without notice. * - * @param {function} opts.authUploadDeviceSigningKeys Function + * @param authUploadDeviceSigningKeys - Function * called to await an interactive auth flow when uploading device signing keys. - * @param {boolean} [opts.setupNewCrossSigning] Optional. Reset even if keys + * @param setupNewCrossSigning - Optional. Reset even if keys * already exist. * Args: - * {function} A function that makes the request requiring auth. Receives the + * A function that makes the request requiring auth. Receives the * auth data as an object. Can be called multiple times, first with an empty * authDict, to obtain the flows. */ - - async bootstrapCrossSigning({ authUploadDeviceSigningKeys, setupNewCrossSigning } = {}) { _logger.logger.log("Bootstrapping cross-signing"); - const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks; const builder = new _EncryptionSetup.EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks); - const crossSigningInfo = new _CrossSigning.CrossSigningInfo(this.userId, builder.crossSigningCallbacks, builder.crossSigningCallbacks); // Reset the cross-signing keys + const crossSigningInfo = new _CrossSigning.CrossSigningInfo(this.userId, builder.crossSigningCallbacks, builder.crossSigningCallbacks); + // Reset the cross-signing keys const resetCrossSigning = async () => { - crossSigningInfo.resetKeys(); // Sign master key with device key + crossSigningInfo.resetKeys(); + // Sign master key with device key + await this.signObject(crossSigningInfo.keys.master); - await this.signObject(crossSigningInfo.keys.master); // Store auth flow helper function, as we need to call it when uploading + // Store auth flow helper function, as we need to call it when uploading // to ensure we handle auth errors properly. + builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys); - builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys); // Cross-sign own device - + // Cross-sign own device const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); const deviceSignature = await crossSigningInfo.signDevice(this.userId, device); - builder.addKeySignature(this.userId, this.deviceId, deviceSignature); // Sign message key backup with cross-signing master key + builder.addKeySignature(this.userId, this.deviceId, deviceSignature); + // Sign message key backup with cross-signing master key if (this.backupManager.backupInfo) { await crossSigningInfo.signObject(this.backupManager.backupInfo.auth_data, "master"); builder.addSessionBackup(this.backupManager.backupInfo); } }; - const publicKeysOnDevice = this.crossSigningInfo.getId(); const privateKeysInCache = await this.crossSigningInfo.isStoredInKeyCache(); const privateKeysInStorage = await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage); - const privateKeysExistSomewhere = privateKeysInCache || privateKeysInStorage; // Log all relevant state for easier parsing of debug logs. + const privateKeysExistSomewhere = privateKeysInCache || privateKeysInStorage; + // Log all relevant state for easier parsing of debug logs. _logger.logger.log({ setupNewCrossSigning, publicKeysOnDevice, @@ -665,52 +564,45 @@ privateKeysInStorage, privateKeysExistSomewhere }); - if (!privateKeysExistSomewhere || setupNewCrossSigning) { - _logger.logger.log("Cross-signing private keys not found locally or in secret storage, " + "creating new keys"); // If a user has multiple devices, it important to only call bootstrap + _logger.logger.log("Cross-signing private keys not found locally or in secret storage, " + "creating new keys"); + // If a user has multiple devices, it important to only call bootstrap // as part of some UI flow (and not silently during startup), as they // may have setup cross-signing on a platform which has not saved keys // to secret storage, and this would reset them. In such a case, you // should prompt the user to verify any existing devices first (and // request private keys from those devices) before calling bootstrap. - - await resetCrossSigning(); } else if (publicKeysOnDevice && privateKeysInCache) { _logger.logger.log("Cross-signing public keys trusted and private keys found locally"); } else if (privateKeysInStorage) { _logger.logger.log("Cross-signing private keys not found locally, but they are available " + "in secret storage, reading storage and caching locally"); - await this.checkOwnCrossSigningTrust({ allowPrivateKeyRequests: true }); - } // Assuming no app-supplied callback, default to storing new private keys in + } + + // Assuming no app-supplied callback, default to storing new private keys in // secret storage if it exists. If it does not, it is assumed this will be // done as part of setting up secret storage later. - - const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys; - if (crossSigningPrivateKeys.size && !this.baseApis.cryptoCallbacks.saveCrossSigningKeys) { - const secretStorage = new _SecretStorage.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks); - + const secretStorage = new _SecretStorage.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks, undefined); if (await secretStorage.hasKey()) { - _logger.logger.log("Storing new cross-signing private keys in secret storage"); // This is writing to in-memory account data in + _logger.logger.log("Storing new cross-signing private keys in secret storage"); + // This is writing to in-memory account data in // builder.accountDataClientAdapter so won't fail - - await _CrossSigning.CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage); } } - const operation = builder.buildOperation(); - await operation.apply(this); // This persists private keys and public keys as trusted, + await operation.apply(this); + // This persists private keys and public keys as trusted, // only do this if apply succeeded for now as retry isn't in place yet - await builder.persist(this); - _logger.logger.log("Cross-signing ready"); } + /** * Bootstrap Secure Secret Storage if needed by creating a default key. If everything is * already set up, then no changes are made, so this is safe to run to ensure secret @@ -726,28 +618,25 @@ * * The Secure Secret Storage API is currently UNSTABLE and may change without notice. * - * @param {function} [opts.createSecretStorageKey] Optional. Function + * @param createSecretStorageKey - Optional. Function * called to await a secret storage key creation flow. - * Returns: - * {Promise} Object with public key metadata, encoded private + * Returns a Promise which resolves to an object with public key metadata, encoded private * recovery key which should be disposed of after displaying to the user, * and raw private key to avoid round tripping if needed. - * @param {object} [opts.keyBackupInfo] The current key backup object. If passed, + * @param keyBackupInfo - The current key backup object. If passed, * the passphrase and recovery key from this backup will be used. - * @param {boolean} [opts.setupNewKeyBackup] If true, a new key backup version will be + * @param setupNewKeyBackup - If true, a new key backup version will be * created and the private key stored in the new SSSS store. Ignored if keyBackupInfo * is supplied. - * @param {boolean} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist. - * @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's + * @param setupNewSecretStorage - Optional. Reset even if keys already exist. + * @param getKeyBackupPassphrase - Optional. Function called to get the user's * current key backup passphrase. Should return a promise that resolves with a Buffer * containing the key, or rejects if the key cannot be obtained. * Returns: - * {Promise} A promise which resolves to key creation data for + * A promise which resolves to key creation data for * SecretStorage#addKey: an object with `passphrase` etc fields. */ // TODO this does not resolve with what it says it does - - async bootstrapSecretStorage({ createSecretStorageKey = async () => ({}), keyBackupInfo, @@ -756,40 +645,36 @@ getKeyBackupPassphrase } = {}) { _logger.logger.log("Bootstrapping Secure Secret Storage"); - const delegateCryptoCallbacks = this.baseApis.cryptoCallbacks; const builder = new _EncryptionSetup.EncryptionSetupBuilder(this.baseApis.store.accountData, delegateCryptoCallbacks); - const secretStorage = new _SecretStorage.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks); // the ID of the new SSSS key, if we create one + const secretStorage = new _SecretStorage.SecretStorage(builder.accountDataClientAdapter, builder.ssssCryptoCallbacks, undefined); - let newKeyId = null; // create a new SSSS key and set it as default + // the ID of the new SSSS key, if we create one + let newKeyId = null; + // create a new SSSS key and set it as default const createSSSS = async (opts, privateKey) => { if (privateKey) { opts.key = privateKey; } - const { keyId, keyInfo } = await secretStorage.addKey(_SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES, opts); - if (privateKey) { // make the private key available to encrypt 4S secrets builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey); } - await secretStorage.setDefaultKeyId(keyId); return keyId; }; - const ensureCanCheckPassphrase = async (keyId, keyInfo) => { if (!keyInfo.mac) { - const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey({ + const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.({ keys: { [keyId]: keyInfo } }, ""); - if (key) { const privateKey = key[1]; builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey); @@ -803,12 +688,10 @@ } } }; - const signKeyBackupWithCrossSigning = async keyBackupAuthData => { if (this.crossSigningInfo.getId() && (await this.crossSigningInfo.isStoredInKeyCache("master"))) { try { _logger.logger.log("Adding cross-signing signature to key backup"); - await this.crossSigningInfo.signObject(keyBackupAuthData, "master"); } catch (e) { // This step is not critical (just helpful), so we catch here @@ -819,11 +702,11 @@ _logger.logger.warn("Cross-signing keys not available, skipping signature on key backup"); } }; - const oldSSSSKey = await this.getSecretStorageKey(); const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null]; - const storageExists = !setupNewSecretStorage && oldKeyInfo && oldKeyInfo.algorithm === _SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES; // Log all relevant state for easier parsing of debug logs. + const storageExists = !setupNewSecretStorage && oldKeyInfo && oldKeyInfo.algorithm === _SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES; + // Log all relevant state for easier parsing of debug logs. _logger.logger.log({ keyBackupInfo, setupNewKeyBackup, @@ -831,18 +714,17 @@ storageExists, oldKeyInfo }); - if (!storageExists && !keyBackupInfo) { // either we don't have anything, or we've been asked to restart // from scratch - _logger.logger.log("Secret storage does not exist, creating new storage key"); // if we already have a usable default SSSS key and aren't resetting + _logger.logger.log("Secret storage does not exist, creating new storage key"); + + // if we already have a usable default SSSS key and aren't resetting // SSSS just use it. otherwise, create a new one // Note: we leave the old SSSS key in place: there could be other // secrets using it, in theory. We could move them to the new key but a) // that would mean we'd need to prompt for the old passphrase, and b) // it's not clear that would be the right thing to do anyway. - - const { keyInfo = {}, privateKey @@ -850,14 +732,14 @@ newKeyId = await createSSSS(keyInfo, privateKey); } else if (!storageExists && keyBackupInfo) { // we have an existing backup, but no SSSS - _logger.logger.log("Secret storage does not exist, using key backup key"); // if we have the backup key already cached, use it; otherwise use the - // callback to prompt for the key + _logger.logger.log("Secret storage does not exist, using key backup key"); + // if we have the backup key already cached, use it; otherwise use the + // callback to prompt for the key + const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase?.()); - const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase()); // create a new SSSS key and use the backup key as the new SSSS key - + // create a new SSSS key and use the backup key as the new SSSS key const opts = {}; - if (keyBackupInfo.auth_data.private_key_salt && keyBackupInfo.auth_data.private_key_iterations) { // FIXME: ??? opts.passphrase = { @@ -867,259 +749,228 @@ bits: 256 }; } + newKeyId = await createSSSS(opts, backupKey); - newKeyId = await createSSSS(opts, backupKey); // store the backup key in secret storage + // store the backup key in secret storage + await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]); - await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId]); // The backup is trusted because the user provided the private key. + // The backup is trusted because the user provided the private key. // Sign the backup with the cross-signing key so the key backup can // be trusted via cross-signing. - await signKeyBackupWithCrossSigning(keyBackupInfo.auth_data); builder.addSessionBackup(keyBackupInfo); } else { // 4S is already set up _logger.logger.log("Secret storage exists"); - if (oldKeyInfo && oldKeyInfo.algorithm === _SecretStorage.SECRET_STORAGE_ALGORITHM_V1_AES) { // make sure that the default key has the information needed to // check the passphrase await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo); } - } // If we have cross-signing private keys cached, store them in secret - // storage if they are not there already. - + } + // If we have cross-signing private keys cached, store them in secret + // storage if they are not there already. if (!this.baseApis.cryptoCallbacks.saveCrossSigningKeys && (await this.isCrossSigningReady()) && (newKeyId || !(await this.crossSigningInfo.isStoredInSecretStorage(secretStorage)))) { _logger.logger.log("Copying cross-signing private keys from cache to secret storage"); - - const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache(); // This is writing to in-memory account data in + const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache(); + // This is writing to in-memory account data in // builder.accountDataClientAdapter so won't fail - await _CrossSigning.CrossSigningInfo.storeInSecretStorage(crossSigningPrivateKeys, secretStorage); } - if (setupNewKeyBackup && !keyBackupInfo) { _logger.logger.log("Creating new message key backup version"); - - const info = await this.baseApis.prepareKeyBackupVersion(null - /* random key */ - , // don't write to secret storage, as it will write to this.secretStorage. + const info = await this.baseApis.prepareKeyBackupVersion(null /* random key */, + // don't write to secret storage, as it will write to this.secretStorage. // Here, we want to capture all the side-effects of bootstrapping, // and want to write to the local secretStorage object { secureSecretStorage: false - }); // write the key ourselves to 4S - + }); + // write the key ourselves to 4S const privateKey = (0, _recoverykey.decodeRecoveryKey)(info.recovery_key); - await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey)); // create keyBackupInfo object to add to builder + await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey)); + // create keyBackupInfo object to add to builder const data = { algorithm: info.algorithm, auth_data: info.auth_data - }; // Sign with cross-signing master key + }; - await signKeyBackupWithCrossSigning(data.auth_data); // sign with the device fingerprint + // Sign with cross-signing master key + await signKeyBackupWithCrossSigning(data.auth_data); + // sign with the device fingerprint await this.signObject(data.auth_data); builder.addSessionBackup(data); - } // Cache the session backup key - - - const sessionBackupKey = await secretStorage.get('m.megolm_backup.v1'); + } + // Cache the session backup key + const sessionBackupKey = await secretStorage.get("m.megolm_backup.v1"); if (sessionBackupKey) { - _logger.logger.info("Got session backup key from secret storage: caching"); // fix up the backup key if it's in the wrong format, and replace + _logger.logger.info("Got session backup key from secret storage: caching"); + // fix up the backup key if it's in the wrong format, and replace // in secret storage - - const fixedBackupKey = fixBackupKey(sessionBackupKey); - if (fixedBackupKey) { - await secretStorage.store("m.megolm_backup.v1", fixedBackupKey, [newKeyId || oldKeyId]); + const keyId = newKeyId || oldKeyId; + await secretStorage.store("m.megolm_backup.v1", fixedBackupKey, keyId ? [keyId] : null); } - const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(fixedBackupKey || sessionBackupKey)); builder.addSessionBackupPrivateKeyToCache(decodedBackupKey); } else if (this.backupManager.getKeyBackupEnabled()) { // key backup is enabled but we don't have a session backup key in SSSS: see if we have one in // the cache or the user can provide one, and if so, write it to SSSS - const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase()); - + const backupKey = (await this.getSessionBackupPrivateKey()) || (await getKeyBackupPassphrase?.()); if (!backupKey) { // This will require user intervention to recover from since we don't have the key // backup key anywhere. The user should probably just set up a new key backup and // the key for the new backup will be stored. If we hit this scenario in the wild // with any frequency, we should do more than just log an error. _logger.logger.error("Key backup is enabled but couldn't get key backup key!"); - return; } - _logger.logger.info("Got session backup key from cache/user that wasn't in SSSS: saving to SSSS"); - await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey)); } - const operation = builder.buildOperation(); - await operation.apply(this); // this persists private keys and public keys as trusted, + await operation.apply(this); + // this persists private keys and public keys as trusted, // only do this if apply succeeded for now as retry isn't in place yet - await builder.persist(this); - _logger.logger.log("Secure Secret Storage ready"); } - addSecretStorageKey(algorithm, opts, keyID) { return this.secretStorage.addKey(algorithm, opts, keyID); } - hasSecretStorageKey(keyID) { return this.secretStorage.hasKey(keyID); } - getSecretStorageKey(keyID) { return this.secretStorage.getKey(keyID); } - storeSecret(name, secret, keys) { return this.secretStorage.store(name, secret, keys); } - getSecret(name) { return this.secretStorage.get(name); } - isSecretStored(name) { return this.secretStorage.isStored(name); } - requestSecret(name, devices) { if (!devices) { devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(this.userId)); } - return this.secretStorage.request(name, devices); } - getDefaultSecretStorageKeyId() { return this.secretStorage.getDefaultKeyId(); } - setDefaultSecretStorageKeyId(k) { return this.secretStorage.setDefaultKeyId(k); } - checkSecretStorageKey(key, info) { return this.secretStorage.checkKey(key, info); } + /** * Checks that a given secret storage private key matches a given public key. * This can be used by the getSecretStorageKey callback to verify that the * private key it is about to supply is the one that was requested. * - * @param {Uint8Array} privateKey The private key - * @param {string} expectedPublicKey The public key - * @returns {boolean} true if the key matches, otherwise false + * @param privateKey - The private key + * @param expectedPublicKey - The public key + * @returns true if the key matches, otherwise false */ - - checkSecretStoragePrivateKey(privateKey, expectedPublicKey) { let decryption = null; - try { decryption = new global.Olm.PkDecryption(); - const gotPubkey = decryption.init_with_private_key(privateKey); // make sure it agrees with the given pubkey - + const gotPubkey = decryption.init_with_private_key(privateKey); + // make sure it agrees with the given pubkey return gotPubkey === expectedPublicKey; } finally { - if (decryption) decryption.free(); + decryption?.free(); } } + /** * Fetches the backup private key, if cached - * @returns {Promise} the key, if any, or null + * @returns the key, if any, or null */ - - async getSessionBackupPrivateKey() { let key = await new Promise(resolve => { // TODO types - this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.cryptoStore.getSecretStorePrivateKey(txn, resolve, "m.megolm_backup.v1"); }); - }); // make sure we have a Uint8Array, rather than a string + }); + // make sure we have a Uint8Array, rather than a string if (key && typeof key === "string") { key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(key) || key)); await this.storeSessionBackupPrivateKey(key); } - if (key && key.ciphertext) { const pickleKey = Buffer.from(this.olmDevice.pickleKey); const decrypted = await (0, _aes.decryptAES)(key, pickleKey, "m.megolm_backup.v1"); key = olmlib.decodeBase64(decrypted); } - return key; } + /** * Stores the session backup key to the cache - * @param {Uint8Array} key the private key - * @returns {Promise} so you can catch failures + * @param key - the private key + * @returns a promise so you can catch failures */ - - async storeSessionBackupPrivateKey(key) { if (!(key instanceof Uint8Array)) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`); } - const pickleKey = Buffer.from(this.olmDevice.pickleKey); const encryptedKey = await (0, _aes.encryptAES)(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1"); - return this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + return this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey); }); } + /** * Checks that a given cross-signing private key matches a given public key. * This can be used by the getCrossSigningKey callback to verify that the * private key it is about to supply is the one that was requested. * - * @param {Uint8Array} privateKey The private key - * @param {string} expectedPublicKey The public key - * @returns {boolean} true if the key matches, otherwise false + * @param privateKey - The private key + * @param expectedPublicKey - The public key + * @returns true if the key matches, otherwise false */ - - checkCrossSigningPrivateKey(privateKey, expectedPublicKey) { let signing = null; - try { signing = new global.Olm.PkSigning(); - const gotPubkey = signing.init_with_seed(privateKey); // make sure it agrees with the given pubkey - + const gotPubkey = signing.init_with_seed(privateKey); + // make sure it agrees with the given pubkey return gotPubkey === expectedPublicKey; } finally { - if (signing) signing.free(); + signing?.free(); } } + /** * Run various follow-up actions after cross-signing keys have changed locally * (either by resetting the keys for the account or by getting them from secret * storage), such as signing the current device, upgrading device * verifications, etc. */ - - async afterCrossSigningLocalKeyChange() { - _logger.logger.info("Starting cross-signing key change post-processing"); // sign the current device with the new key, and upload to the server - + _logger.logger.info("Starting cross-signing key change post-processing"); + // sign the current device with the new key, and upload to the server const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device); - _logger.logger.info(`Starting background key sig upload for ${this.deviceId}`); - const upload = ({ shouldEmit = false }) => { @@ -1131,7 +982,6 @@ const { failures } = response || {}; - if (Object.keys(failures || []).length > 0) { if (shouldEmit) { this.baseApis.emit(CryptoEvent.KeySignatureUploadFailure, failures, "afterCrossSigningLocalKeyChange", upload // continuation @@ -1142,41 +992,33 @@ failures }); } - _logger.logger.info(`Finished background key sig upload for ${this.deviceId}`); }).catch(e => { _logger.logger.error(`Error during background key sig upload for ${this.deviceId}`, e); }); }; - upload({ shouldEmit: true }); const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications; - if (shouldUpgradeCb) { - _logger.logger.info("Starting device verification upgrade"); // Check all users for signatures if upgrade callback present - // FIXME: do this in batches - + _logger.logger.info("Starting device verification upgrade"); + // Check all users for signatures if upgrade callback present + // FIXME: do this in batches const users = {}; - for (const [userId, crossSigningInfo] of Object.entries(this.deviceList.crossSigningInfo)) { const upgradeInfo = await this.checkForDeviceVerificationUpgrade(userId, _CrossSigning.CrossSigningInfo.fromStorage(crossSigningInfo, userId)); - if (upgradeInfo) { users[userId] = upgradeInfo; } } - if (Object.keys(users).length > 0) { _logger.logger.info(`Found ${Object.keys(users).length} verif users to upgrade`); - try { const usersToUpgrade = await shouldUpgradeCb({ users: users }); - if (usersToUpgrade) { for (const userId of usersToUpgrade) { if (userId in users) { @@ -1188,30 +1030,25 @@ _logger.logger.log("shouldUpgradeDeviceVerifications threw an error: not upgrading", e); } } - _logger.logger.info("Finished device verification upgrade"); } - _logger.logger.info("Finished cross-signing key change post-processing"); } + /** * Check if a user's cross-signing key is a candidate for upgrading from device * verification. * - * @param {string} userId the user whose cross-signing information is to be checked - * @param {object} crossSigningInfo the cross-signing information to check + * @param userId - the user whose cross-signing information is to be checked + * @param crossSigningInfo - the cross-signing information to check */ - - async checkForDeviceVerificationUpgrade(userId, crossSigningInfo) { // only upgrade if this is the first cross-signing key that we've seen for // them, and if their cross-signing key isn't already verified const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo); - if (crossSigningInfo.firstUse && !trustLevel.isVerified()) { const devices = this.deviceList.getRawStoredDevicesForUser(userId); const deviceIds = await this.checkForValidDeviceSignature(userId, crossSigningInfo.keys.master, devices); - if (deviceIds.length) { return { devices: deviceIds.map(deviceId => _deviceinfo.DeviceInfo.fromStorage(devices[deviceId], deviceId)), @@ -1220,23 +1057,20 @@ } } } + /** * Check if the cross-signing key is signed by a verified device. * - * @param {string} userId the user ID whose key is being checked - * @param {object} key the key that is being checked - * @param {object} devices the user's devices. Should be a map from device ID + * @param userId - the user ID whose key is being checked + * @param key - the key that is being checked + * @param devices - the user's devices. Should be a map from device ID * to device info */ - - async checkForValidDeviceSignature(userId, key, devices) { const deviceIds = []; - if (devices && key.signatures && key.signatures[userId]) { for (const signame of Object.keys(key.signatures[userId])) { - const [, deviceId] = signame.split(':', 2); - + const [, deviceId] = signame.split(":", 2); if (deviceId in devices && devices[deviceId].verified === DeviceVerification.VERIFIED) { try { await olmlib.verifySignature(this.olmDevice, key, userId, deviceId, devices[deviceId].keys[signame]); @@ -1245,80 +1079,71 @@ } } } - return deviceIds; } + /** * Get the user's cross-signing key ID. * - * @param {string} [type=master] The type of key to get the ID of. One of + * @param type - The type of key to get the ID of. One of * "master", "self_signing", or "user_signing". Defaults to "master". * - * @returns {string} the key ID + * @returns the key ID */ - - getCrossSigningId(type) { return this.crossSigningInfo.getId(type); } + /** * Get the cross signing information for a given user. * - * @param {string} userId the user ID to get the cross-signing info for. + * @param userId - the user ID to get the cross-signing info for. * - * @returns {CrossSigningInfo} the cross signing information for the user. + * @returns the cross signing information for the user. */ - - getStoredCrossSigningForUser(userId) { return this.deviceList.getStoredCrossSigningForUser(userId); } + /** * Check whether a given user is trusted. * - * @param {string} userId The ID of the user to check. + * @param userId - The ID of the user to check. * - * @returns {UserTrustLevel} + * @returns */ - - checkUserTrust(userId) { const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); - if (!userCrossSigning) { return new _CrossSigning.UserTrustLevel(false, false, false); } - return this.crossSigningInfo.checkUserTrust(userCrossSigning); } + /** * Check whether a given device is trusted. * - * @param {string} userId The ID of the user whose devices is to be checked. - * @param {string} deviceId The ID of the device to check + * @param userId - The ID of the user whose devices is to be checked. + * @param deviceId - The ID of the device to check * - * @returns {DeviceTrustLevel} + * @returns */ - - checkDeviceTrust(userId, deviceId) { const device = this.deviceList.getStoredDevice(userId, deviceId); return this.checkDeviceInfoTrust(userId, device); } + /** * Check whether a given deviceinfo is trusted. * - * @param {string} userId The ID of the user whose devices is to be checked. - * @param {module:crypto/deviceinfo?} device The device info object to check + * @param userId - The ID of the user whose devices is to be checked. + * @param device - The device info object to check * - * @returns {DeviceTrustLevel} + * @returns */ - - checkDeviceInfoTrust(userId, device) { - const trustedLocally = !!(device && device.isVerified()); + const trustedLocally = !!device?.isVerified(); const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); - if (device && userCrossSigning) { // The trustCrossSignedDevices only affects trust of other people's cross-signing // signatures @@ -1328,26 +1153,26 @@ return new _CrossSigning.DeviceTrustLevel(false, false, trustedLocally, false); } } + /** * Check whether one of our own devices is cross-signed by our * user's stored keys, regardless of whether we trust those keys yet. * - * @param {string} deviceId The ID of the device to check + * @param deviceId - The ID of the device to check * - * @returns {boolean} true if the device is cross-signed + * @returns true if the device is cross-signed */ - - checkIfOwnDeviceCrossSigned(deviceId) { const device = this.deviceList.getStoredDevice(this.userId, deviceId); + if (!device) return false; const userCrossSigning = this.deviceList.getStoredCrossSigningForUser(this.userId); - return userCrossSigning.checkDeviceTrust(userCrossSigning, device, false, true).isCrossSigningVerified(); + return userCrossSigning?.checkDeviceTrust(userCrossSigning, device, false, true).isCrossSigningVerified() ?? false; } + /* * Event handler for DeviceList's userNewDevices event */ - /** * Check the copy of our cross-signing key that we have in the device list and * see if we can get the private key. If so, mark it as trusted. @@ -1355,107 +1180,92 @@ async checkOwnCrossSigningTrust({ allowPrivateKeyRequests = false } = {}) { - const userId = this.userId; // Before proceeding, ensure our cross-signing public keys have been + const userId = this.userId; + + // Before proceeding, ensure our cross-signing public keys have been // downloaded via the device list. + await this.downloadKeys([this.userId]); - await this.downloadKeys([this.userId]); // Also check which private keys are locally cached. + // Also check which private keys are locally cached. + const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache(); - const crossSigningPrivateKeys = await this.crossSigningInfo.getCrossSigningKeysFromCache(); // If we see an update to our own master key, check it against the master + // If we see an update to our own master key, check it against the master // key we have and, if it matches, mark it as verified - // First, get the new cross-signing info + // First, get the new cross-signing info const newCrossSigning = this.deviceList.getStoredCrossSigningForUser(userId); - if (!newCrossSigning) { _logger.logger.error("Got cross-signing update event for user " + userId + " but no new cross-signing information found!"); - return; } - const seenPubkey = newCrossSigning.getId(); const masterChanged = this.crossSigningInfo.getId() !== seenPubkey; const masterExistsNotLocallyCached = newCrossSigning.getId() && !crossSigningPrivateKeys.has("master"); - if (masterChanged) { _logger.logger.info("Got new master public key", seenPubkey); } - if (allowPrivateKeyRequests && (masterChanged || masterExistsNotLocallyCached)) { _logger.logger.info("Attempting to retrieve cross-signing master private key"); - - let signing = null; // It's important for control flow that we leave any errors alone for + let signing = null; + // It's important for control flow that we leave any errors alone for // higher levels to handle so that e.g. cancelling access properly // aborts any larger operation as well. - try { - const ret = await this.crossSigningInfo.getCrossSigningKey('master', seenPubkey); + const ret = await this.crossSigningInfo.getCrossSigningKey("master", seenPubkey); signing = ret[1]; - _logger.logger.info("Got cross-signing master private key"); } finally { - if (signing) signing.free(); + signing?.free(); } } - const oldSelfSigningId = this.crossSigningInfo.getId("self_signing"); - const oldUserSigningId = this.crossSigningInfo.getId("user_signing"); // Update the version of our keys in our cross-signing object and the local store + const oldUserSigningId = this.crossSigningInfo.getId("user_signing"); + // Update the version of our keys in our cross-signing object and the local store this.storeTrustedSelfKeys(newCrossSigning.keys); const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing"); const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing"); const selfSigningExistsNotLocallyCached = newCrossSigning.getId("self_signing") && !crossSigningPrivateKeys.has("self_signing"); const userSigningExistsNotLocallyCached = newCrossSigning.getId("user_signing") && !crossSigningPrivateKeys.has("user_signing"); const keySignatures = {}; - if (selfSigningChanged) { _logger.logger.info("Got new self-signing key", newCrossSigning.getId("self_signing")); } - if (allowPrivateKeyRequests && (selfSigningChanged || selfSigningExistsNotLocallyCached)) { _logger.logger.info("Attempting to retrieve cross-signing self-signing private key"); - let signing = null; - try { const ret = await this.crossSigningInfo.getCrossSigningKey("self_signing", newCrossSigning.getId("self_signing")); signing = ret[1]; - _logger.logger.info("Got cross-signing self-signing private key"); } finally { - if (signing) signing.free(); + signing?.free(); } - const device = this.deviceList.getStoredDevice(this.userId, this.deviceId); const signedDevice = await this.crossSigningInfo.signDevice(this.userId, device); keySignatures[this.deviceId] = signedDevice; } - if (userSigningChanged) { _logger.logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); } - if (allowPrivateKeyRequests && (userSigningChanged || userSigningExistsNotLocallyCached)) { _logger.logger.info("Attempting to retrieve cross-signing user-signing private key"); - let signing = null; - try { const ret = await this.crossSigningInfo.getCrossSigningKey("user_signing", newCrossSigning.getId("user_signing")); signing = ret[1]; - _logger.logger.info("Got cross-signing user-signing private key"); } finally { - if (signing) signing.free(); + signing?.free(); } } - if (masterChanged) { const masterKey = this.crossSigningInfo.keys.master; await this.signObject(masterKey); - const deviceSig = masterKey.signatures[this.userId]["ed25519:" + this.deviceId]; // Include only the _new_ device signature in the upload. + const deviceSig = masterKey.signatures[this.userId]["ed25519:" + this.deviceId]; + // Include only the _new_ device signature in the upload. // We may have existing signatures from deleted devices, which will cause // the entire upload to fail. - keySignatures[this.crossSigningInfo.getId()] = Object.assign({}, masterKey, { signatures: { [this.userId]: { @@ -1464,29 +1274,23 @@ } }); } - const keysToUpload = Object.keys(keySignatures); - if (keysToUpload.length) { const upload = ({ shouldEmit = false }) => { _logger.logger.info(`Starting background key sig upload for ${keysToUpload}`); - return this.baseApis.uploadKeySignatures({ [this.userId]: keySignatures }).then(response => { const { failures } = response || {}; - _logger.logger.info(`Finished background key sig upload for ${keysToUpload}`); - if (Object.keys(failures || []).length > 0) { if (shouldEmit) { this.baseApis.emit(CryptoEvent.KeySignatureUploadFailure, failures, "checkOwnCrossSigningTrust", upload); } - throw new _errors.KeySignatureUploadError("Key upload failed", { failures }); @@ -1495,188 +1299,150 @@ _logger.logger.error(`Error during background key sig upload for ${keysToUpload}`, e); }); }; - upload({ shouldEmit: true }); } - this.emit(CryptoEvent.UserTrustStatusChanged, userId, this.checkUserTrust(userId)); - if (masterChanged) { this.emit(CryptoEvent.KeysChanged, {}); await this.afterCrossSigningLocalKeyChange(); - } // Now we may be able to trust our key backup - + } - await this.backupManager.checkKeyBackup(); // FIXME: if we previously trusted the backup, should we automatically sign + // Now we may be able to trust our key backup + await this.backupManager.checkKeyBackup(); + // FIXME: if we previously trusted the backup, should we automatically sign // the backup with the new key (if not already signed)? } + /** * Store a set of keys as our own, trusted, cross-signing keys. * - * @param {object} keys The new trusted set of keys + * @param keys - The new trusted set of keys */ - - async storeTrustedSelfKeys(keys) { if (keys) { this.crossSigningInfo.setKeys(keys); } else { this.crossSigningInfo.clearKeys(); } - - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.cryptoStore.storeCrossSigningKeys(txn, this.crossSigningInfo.keys); }); } + /** * Check if the master key is signed by a verified device, and if so, prompt * the application to mark it as verified. * - * @param {string} userId the user ID whose key should be checked + * @param userId - the user ID whose key should be checked */ - - async checkDeviceVerifications(userId) { const shouldUpgradeCb = this.baseApis.cryptoCallbacks.shouldUpgradeDeviceVerifications; - if (!shouldUpgradeCb) { // Upgrading skipped when callback is not present. return; } - _logger.logger.info(`Starting device verification upgrade for ${userId}`); - if (this.crossSigningInfo.keys.user_signing) { const crossSigningInfo = this.deviceList.getStoredCrossSigningForUser(userId); - if (crossSigningInfo) { const upgradeInfo = await this.checkForDeviceVerificationUpgrade(userId, crossSigningInfo); - if (upgradeInfo) { const usersToUpgrade = await shouldUpgradeCb({ users: { [userId]: upgradeInfo } }); - if (usersToUpgrade.includes(userId)) { await this.baseApis.setDeviceVerified(userId, crossSigningInfo.getId()); } } } } - _logger.logger.info(`Finished device verification upgrade for ${userId}`); } + /** */ - - enableLazyLoading() { this.lazyLoadMembers = true; } + /** * Tell the crypto module to register for MatrixClient events which it needs to * listen for * - * @param {external:EventEmitter} eventEmitter event source where we can register + * @param eventEmitter - event source where we can register * for event notifications */ - - registerEventHandlers(eventEmitter) { eventEmitter.on(_roomMember.RoomMemberEvent.Membership, this.onMembership); eventEmitter.on(_client.ClientEvent.ToDeviceEvent, this.onToDeviceEvent); eventEmitter.on(_room.RoomEvent.Timeline, this.onTimelineEvent); eventEmitter.on(_event2.MatrixEventEvent.Decrypted, this.onTimelineEvent); } - /** Start background processes related to crypto */ - + /** + * @deprecated this does nothing and will be removed in a future version + */ start() { - this.outgoingRoomKeyRequestManager.start(); + _logger.logger.warn("MatrixClient.crypto.start() is deprecated"); } - /** Stop background processes related to crypto */ - + /** Stop background processes related to crypto */ stop() { this.outgoingRoomKeyRequestManager.stop(); this.deviceList.stop(); this.dehydrationManager.stop(); } + /** * Get the Ed25519 key for this device * - * @return {string} base64-encoded ed25519 key. + * @returns base64-encoded ed25519 key. */ - - getDeviceEd25519Key() { return this.olmDevice.deviceEd25519Key; } + /** * Get the Curve25519 key for this device * - * @return {string} base64-encoded curve25519 key. + * @returns base64-encoded curve25519 key. */ - - getDeviceCurve25519Key() { return this.olmDevice.deviceCurve25519Key; } + /** * Set the global override for whether the client should ever send encrypted * messages to unverified devices. This provides the default for rooms which * do not specify a value. * - * @param {boolean} value whether to blacklist all unverified devices by default + * @param value - whether to blacklist all unverified devices by default + * + * @deprecated For external code, use {@link MatrixClient#setGlobalBlacklistUnverifiedDevices}. For + * internal code, set {@link MatrixClient#globalBlacklistUnverifiedDevices} directly. */ - - setGlobalBlacklistUnverifiedDevices(value) { this.globalBlacklistUnverifiedDevices = value; } - /** - * @return {boolean} whether to blacklist all unverified devices by default - */ - - getGlobalBlacklistUnverifiedDevices() { - return this.globalBlacklistUnverifiedDevices; - } /** - * Set whether sendMessage in a room with unknown and unverified devices - * should throw an error and not send them message. This has 'Global' for - * symmetry with setGlobalBlacklistUnverifiedDevices but there is currently - * no room-level equivalent for this setting. + * @returns whether to blacklist all unverified devices by default * - * This API is currently UNSTABLE and may change or be removed without notice. - * - * @param {boolean} value whether error on unknown devices + * @deprecated For external code, use {@link MatrixClient#getGlobalBlacklistUnverifiedDevices}. For + * internal code, reference {@link MatrixClient#globalBlacklistUnverifiedDevices} directly. */ - - - setGlobalErrorOnUnknownDevices(value) { - this.globalErrorOnUnknownDevices = value; + getGlobalBlacklistUnverifiedDevices() { + return this.globalBlacklistUnverifiedDevices; } - /** - * @return {boolean} whether to error on unknown devices - * - * This API is currently UNSTABLE and may change or be removed without notice. - */ - - getGlobalErrorOnUnknownDevices() { - return this.globalErrorOnUnknownDevices; - } /** * Upload the device keys to the homeserver. - * @return {object} A promise that will resolve when the keys are uploaded. + * @returns A promise that will resolve when the keys are uploaded. */ - - uploadDeviceKeys() { const deviceKeys = { algorithms: this.supportedAlgorithms, @@ -1690,14 +1456,13 @@ }); }); } + /** * Stores the current one_time_key count which will be handled later (in a call of * onSyncCompleted). The count is e.g. coming from a /sync response. * - * @param {Number} currentCount The current count of one_time_keys to be stored + * @param currentCount - The current count of one_time_keys to be stored */ - - updateOneTimeKeyCount(currentCount) { if (isFinite(currentCount)) { this.oneTimeKeyCount = currentCount; @@ -1705,38 +1470,34 @@ throw new TypeError("Parameter for updateOneTimeKeyCount has to be a number"); } } - setNeedsNewFallback(needsNewFallback) { - this.needsNewFallback = !!needsNewFallback; + this.needsNewFallback = needsNewFallback; } - getNeedsNewFallback() { - return this.needsNewFallback; - } // check if it's time to upload one-time keys, and do so if so. - + return !!this.needsNewFallback; + } + // check if it's time to upload one-time keys, and do so if so. maybeUploadOneTimeKeys() { // frequency with which to check & upload one-time keys const uploadPeriod = 1000 * 60; // one minute + // max number of keys to upload at once // Creating keys can be an expensive operation so we limit the // number we generate in one go to avoid blocking the application // for too long. - const maxKeysPerCycle = 5; - if (this.oneTimeKeyCheckInProgress) { return; } - const now = Date.now(); - if (this.lastOneTimeKeyCheck !== null && now - this.lastOneTimeKeyCheck < uploadPeriod) { // we've done a key upload recently. return; } + this.lastOneTimeKeyCheck = now; - this.lastOneTimeKeyCheck = now; // We need to keep a pool of one time public keys on the server so that + // We need to keep a pool of one time public keys on the server so that // other devices can start conversations with us. But we can only store // a finite number of private keys in the olm Account object. // To complicate things further then can be a delay between a device @@ -1747,35 +1508,31 @@ // private keys clogging up our local storage. // So we need some kind of engineering compromise to balance all of // these factors. - // Check how many keys we can store in the Account object. - const maxOneTimeKeys = this.olmDevice.maxNumberOfOneTimeKeys(); // Try to keep at most half that number on the server. This leaves the + // Check how many keys we can store in the Account object. + const maxOneTimeKeys = this.olmDevice.maxNumberOfOneTimeKeys(); + // Try to keep at most half that number on the server. This leaves the // rest of the slots free to hold keys that have been claimed from the // server but we haven't received a message for. // If we run out of slots when generating new keys then olm will // discard the oldest private keys first. This will eventually clean // out stale private keys that won't receive a message. - const keyLimit = Math.floor(maxOneTimeKeys / 2); - const uploadLoop = async keyCount => { while (keyLimit > keyCount || this.getNeedsNewFallback()) { // Ask olm to generate new one time keys, then upload them to synapse. if (keyLimit > keyCount) { _logger.logger.info("generating oneTimeKeys"); - const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle); await this.olmDevice.generateOneTimeKeys(keysThisLoop); } - if (this.getNeedsNewFallback()) { - const fallbackKeys = await this.olmDevice.getFallbackKey(); // if fallbackKeys is non-empty, we've already generated a + const fallbackKeys = await this.olmDevice.getFallbackKey(); + // if fallbackKeys is non-empty, we've already generated a // fallback key, but it hasn't been published yet, so we // can use that instead of generating a new one - if (!fallbackKeys.curve25519 || Object.keys(fallbackKeys.curve25519).length == 0) { _logger.logger.info("generating fallback key"); - if (this.fallbackCleanup) { // cancel any pending fallback cleanup because generating // a new fallback key will already drop the old fallback @@ -1784,15 +1541,11 @@ clearTimeout(this.fallbackCleanup); delete this.fallbackCleanup; } - await this.olmDevice.generateFallbackKey(); } } - _logger.logger.info("calling uploadOneTimeKeys"); - const res = await this.uploadOneTimeKeys(); - if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) { // if the response contains a more up to date value use this // for the next loop @@ -1802,16 +1555,14 @@ } } }; - this.oneTimeKeyCheckInProgress = true; Promise.resolve().then(() => { if (this.oneTimeKeyCount !== undefined) { // We already have the current one_time_key count from a /sync response. // Use this value instead of asking the server for the current key count. return Promise.resolve(this.oneTimeKeyCount); - } // ask the server how many keys we have - - + } + // ask the server how many keys we have return this.baseApis.uploadKeysRequest({}).then(res => { return res.one_time_key_counts.signed_curve25519 || 0; }); @@ -1829,17 +1580,15 @@ this.oneTimeKeyCount = undefined; this.oneTimeKeyCheckInProgress = false; }); - } // returns a promise which resolves to the response - + } + // returns a promise which resolves to the response async uploadOneTimeKeys() { const promises = []; let fallbackJson; - if (this.getNeedsNewFallback()) { fallbackJson = {}; const fallbackKeys = await this.olmDevice.getFallbackKey(); - for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) { const k = { key, @@ -1848,13 +1597,10 @@ fallbackJson["signed_curve25519:" + keyId] = k; promises.push(this.signObject(k)); } - this.setNeedsNewFallback(false); } - const oneTimeKeys = await this.olmDevice.getOneTimeKeys(); const oneTimeJson = {}; - for (const keyId in oneTimeKeys.curve25519) { if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) { const k = { @@ -1864,155 +1610,129 @@ promises.push(this.signObject(k)); } } - await Promise.all(promises); const requestBody = { - "one_time_keys": oneTimeJson + one_time_keys: oneTimeJson }; - if (fallbackJson) { requestBody["org.matrix.msc2732.fallback_keys"] = fallbackJson; requestBody["fallback_keys"] = fallbackJson; } - const res = await this.baseApis.uploadKeysRequest(requestBody); - if (fallbackJson) { this.fallbackCleanup = setTimeout(() => { delete this.fallbackCleanup; this.olmDevice.forgetOldFallbackKey(); }, 60 * 60 * 1000); } - await this.olmDevice.markKeysAsPublished(); return res; } + /** * Download the keys for a list of users and stores the keys in the session * store. - * @param {Array} userIds The users to fetch. - * @param {boolean} forceDownload Always download the keys even if cached. + * @param userIds - The users to fetch. + * @param forceDownload - Always download the keys even if cached. * - * @return {Promise} A promise which resolves to a map userId->deviceId->{@link - * module:crypto/deviceinfo|DeviceInfo}. + * @returns A promise which resolves to a map `userId->deviceId->{@link DeviceInfo}`. */ - - downloadKeys(userIds, forceDownload) { - return this.deviceList.downloadKeys(userIds, forceDownload); + return this.deviceList.downloadKeys(userIds, !!forceDownload); } + /** * Get the stored device keys for a user id * - * @param {string} userId the user to list keys for. + * @param userId - the user to list keys for. * - * @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't + * @returns list of devices, or null if we haven't * managed to get a list of devices for this user yet. */ - - getStoredDevicesForUser(userId) { return this.deviceList.getStoredDevicesForUser(userId); } + /** * Get the stored keys for a single device * - * @param {string} userId - * @param {string} deviceId * - * @return {module:crypto/deviceinfo?} device, or undefined + * @returns device, or undefined * if we don't know about this device */ - - getStoredDevice(userId, deviceId) { return this.deviceList.getStoredDevice(userId, deviceId); } + /** * Save the device list, if necessary * - * @param {number} delay Time in ms before which the save actually happens. + * @param delay - Time in ms before which the save actually happens. * By default, the save is delayed for a short period in order to batch * multiple writes, but this behaviour can be disabled by passing 0. * - * @return {Promise} true if the data was saved, false if + * @returns true if the data was saved, false if * it was not (eg. because no changes were pending). The promise * will only resolve once the data is saved, so may take some time * to resolve. */ - - saveDeviceList(delay) { return this.deviceList.saveIfDirty(delay); } + /** * Update the blocked/verified state of the given device * - * @param {string} userId owner of the device - * @param {string} deviceId unique identifier for the device or user's + * @param userId - owner of the device + * @param deviceId - unique identifier for the device or user's * cross-signing public key ID. * - * @param {?boolean} verified whether to mark the device as verified. Null to + * @param verified - whether to mark the device as verified. Null to * leave unchanged. * - * @param {?boolean} blocked whether to mark the device as blocked. Null to + * @param blocked - whether to mark the device as blocked. Null to * leave unchanged. * - * @param {?boolean} known whether to mark that the user has been made aware of + * @param known - whether to mark that the user has been made aware of * the existence of this device. Null to leave unchanged * - * @param {?Record} keys The list of keys that was present + * @param keys - The list of keys that was present * during the device verification. This will be double checked with the list * of keys the given device has currently. * - * @return {Promise} updated DeviceInfo + * @returns updated DeviceInfo */ - - - async setDeviceVerification(userId, deviceId, verified, blocked, known, keys) { - // get rid of any `undefined`s here so we can just check - // for null rather than null or undefined - if (verified === undefined) verified = null; - if (blocked === undefined) blocked = null; - if (known === undefined) known = null; // Check if the 'device' is actually a cross signing key + async setDeviceVerification(userId, deviceId, verified = null, blocked = null, known = null, keys) { + // Check if the 'device' is actually a cross signing key // The js-sdk's verification treats cross-signing keys as devices // and so uses this method to mark them verified. - const xsk = this.deviceList.getStoredCrossSigningForUser(userId); - if (xsk && xsk.getId() === deviceId) { if (blocked !== null || known !== null) { throw new Error("Cannot set blocked or known for a cross-signing key"); } - if (!verified) { throw new Error("Cannot set a cross-signing key as unverified"); } - const gotKeyId = keys ? Object.values(keys)[0] : null; - if (keys && (Object.values(keys).length !== 1 || gotKeyId !== xsk.getId())) { throw new Error(`Key did not match expected value: expected ${xsk.getId()}, got ${gotKeyId}`); } - if (!this.crossSigningInfo.getId() && userId === this.crossSigningInfo.userId) { - this.storeTrustedSelfKeys(xsk.keys); // This will cause our own user trust to change, so emit the event - + this.storeTrustedSelfKeys(xsk.keys); + // This will cause our own user trust to change, so emit the event this.emit(CryptoEvent.UserTrustStatusChanged, this.userId, this.checkUserTrust(userId)); - } // Now sign the master key with our user signing key (unless it's ourself) - + } + // Now sign the master key with our user signing key (unless it's ourself) if (userId !== this.userId) { _logger.logger.info("Master key " + xsk.getId() + " for " + userId + " marked verified. Signing..."); - const device = await this.crossSigningInfo.signUser(xsk); - if (device) { const upload = async ({ shouldEmit = false }) => { _logger.logger.info("Uploading signature for " + userId + "..."); - const response = await this.baseApis.uploadKeySignatures({ [userId]: { [deviceId]: device @@ -2021,24 +1741,22 @@ const { failures } = response || {}; - if (Object.keys(failures || []).length > 0) { if (shouldEmit) { this.baseApis.emit(CryptoEvent.KeySignatureUploadFailure, failures, "setDeviceVerification", upload); } /* Throwing here causes the process to be cancelled and the other - * user to be notified */ - - + * user to be notified */ throw new _errors.KeySignatureUploadError("Key upload failed", { failures }); } }; - await upload({ shouldEmit: true - }); // This will emit events when it comes back down the sync + }); + + // This will emit events when it comes back down the sync // (we could do local echo to speed things up) } @@ -2047,16 +1765,12 @@ return xsk; } } - const devices = this.deviceList.getRawStoredDevicesForUser(userId); - if (!devices || !devices[deviceId]) { throw new Error("Unknown device " + userId + ":" + deviceId); } - const dev = devices[deviceId]; let verificationStatus = dev.verified; - if (verified) { if (keys) { for (const [keyId, key] of Object.entries(keys)) { @@ -2065,51 +1779,43 @@ } } } - verificationStatus = DeviceVerification.VERIFIED; } else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) { verificationStatus = DeviceVerification.UNVERIFIED; } - if (blocked) { verificationStatus = DeviceVerification.BLOCKED; } else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) { verificationStatus = DeviceVerification.UNVERIFIED; } - let knownStatus = dev.known; - if (known !== null) { knownStatus = known; } - if (dev.verified !== verificationStatus || dev.known !== knownStatus) { dev.verified = verificationStatus; dev.known = knownStatus; this.deviceList.storeDevicesForUser(userId, devices); this.deviceList.saveIfDirty(); - } // do cross-signing - + } + // do cross-signing if (verified && userId === this.userId) { - _logger.logger.info("Own device " + deviceId + " marked verified: signing"); // Signing only needed if other device not already signed - + _logger.logger.info("Own device " + deviceId + " marked verified: signing"); + // Signing only needed if other device not already signed let device; const deviceTrust = this.checkDeviceTrust(userId, deviceId); - if (deviceTrust.isCrossSigningVerified()) { _logger.logger.log(`Own device ${deviceId} already cross-signing verified`); } else { device = await this.crossSigningInfo.signDevice(userId, _deviceinfo.DeviceInfo.fromStorage(dev, deviceId)); } - if (device) { const upload = async ({ shouldEmit = false }) => { _logger.logger.info("Uploading signature for " + deviceId); - const response = await this.baseApis.uploadKeySignatures({ [userId]: { [deviceId]: device @@ -2118,7 +1824,6 @@ const { failures } = response || {}; - if (Object.keys(failures || []).length > 0) { if (shouldEmit) { this.baseApis.emit(CryptoEvent.KeySignatureUploadFailure, failures, "setDeviceVerification", upload // continuation @@ -2130,81 +1835,63 @@ }); } }; - await upload({ shouldEmit: true - }); // XXX: we'll need to wait for the device list to be updated + }); + // XXX: we'll need to wait for the device list to be updated } } const deviceObj = _deviceinfo.DeviceInfo.fromStorage(dev, deviceId); - this.emit(CryptoEvent.DeviceVerificationChanged, userId, deviceId, deviceObj); return deviceObj; } - findVerificationRequestDMInProgress(roomId) { return this.inRoomVerificationRequests.findRequestInProgress(roomId); } - getVerificationRequestsToDeviceInProgress(userId) { return this.toDeviceVerificationRequests.getRequestsInProgress(userId); } - requestVerificationDM(userId, roomId) { const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId); - if (existingRequest) { return Promise.resolve(existingRequest); } - const channel = new _InRoomChannel.InRoomChannel(this.baseApis, roomId, userId); return this.requestVerificationWithChannel(userId, channel, this.inRoomVerificationRequests); } - requestVerification(userId, devices) { if (!devices) { devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId)); } - const existingRequest = this.toDeviceVerificationRequests.findRequestInProgress(userId, devices); - if (existingRequest) { return Promise.resolve(existingRequest); } - const channel = new _ToDeviceChannel.ToDeviceChannel(this.baseApis, userId, devices, _ToDeviceChannel.ToDeviceChannel.makeTransactionId()); return this.requestVerificationWithChannel(userId, channel, this.toDeviceVerificationRequests); } - async requestVerificationWithChannel(userId, channel, requestsMap) { - let request = new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis); // if transaction id is already known, add request - + let request = new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis); + // if transaction id is already known, add request if (channel.transactionId) { requestsMap.setRequestByChannel(channel, request); } - - await request.sendRequest(); // don't replace the request created by a racing remote echo - + await request.sendRequest(); + // don't replace the request created by a racing remote echo const racingRequest = requestsMap.getRequestByChannel(channel); - if (racingRequest) { request = racingRequest; } else { _logger.logger.log(`Crypto: adding new request to ` + `requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`); - requestsMap.setRequestByChannel(channel, request); } - return request; } - beginKeyVerification(method, userId, deviceId, transactionId = null) { let request; - if (transactionId) { request = this.toDeviceVerificationRequests.getRequestBySenderAndTxnId(userId, transactionId); - if (!request) { throw new Error(`No request found for user ${userId} with ` + `transactionId ${transactionId}`); } @@ -2214,51 +1901,43 @@ request = new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis); this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request); } - return request.beginKeyVerification(method, { userId, deviceId }); } - async legacyDeviceVerification(userId, deviceId, method) { const transactionId = _ToDeviceChannel.ToDeviceChannel.makeTransactionId(); - const channel = new _ToDeviceChannel.ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId); const request = new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis); this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request); const verifier = request.beginKeyVerification(method, { userId, deviceId - }); // either reject by an error from verify() while sending .start + }); + // either reject by an error from verify() while sending .start // or resolve when the request receives the // local (fake remote) echo for sending the .start event - await Promise.race([verifier.verify(), request.waitFor(r => r.started)]); return request; } + /** * Get information on the active olm sessions with a user *

* Returns a map from device id to an object with keys 'deviceIdKey' (the * device's curve25519 identity key) and 'sessions' (an array of objects in the * same format as that returned by - * {@link module:crypto/OlmDevice#getSessionInfoForDevice}). + * {@link OlmDevice#getSessionInfoForDevice}). *

* This method is provided for debugging purposes. * - * @param {string} userId id of user to inspect - * - * @return {Promise>} + * @param userId - id of user to inspect */ - - async getOlmSessionsForUser(userId) { const devices = this.getStoredDevicesForUser(userId) || []; const result = {}; - - for (let j = 0; j < devices.length; ++j) { - const device = devices[j]; + for (const device of devices) { const deviceKey = device.getIdentityKey(); const sessions = await this.olmDevice.getSessionInfoForDevice(deviceKey); result[device.deviceId] = { @@ -2266,40 +1945,36 @@ sessions: sessions }; } - return result; } + /** * Get the device which sent an event * - * @param {module:models/event.MatrixEvent} event event to be checked - * - * @return {module:crypto/deviceinfo?} + * @param event - event to be checked */ - - getEventSenderDeviceInfo(event) { const senderKey = event.getSenderKey(); const algorithm = event.getWireContent().algorithm; - if (!senderKey || !algorithm) { return null; } - if (event.isKeySourceUntrusted()) { // we got the key for this event from a source that we consider untrusted return null; - } // senderKey is the Curve25519 identity key of the device which the event + } + + // senderKey is the Curve25519 identity key of the device which the event // was sent from. In the case of Megolm, it's actually the Curve25519 // identity key of the device which set up the Megolm session. - const device = this.deviceList.getDeviceByIdentityKey(algorithm, senderKey); - if (device === null) { // we haven't downloaded the details of this device yet. return null; - } // so far so good, but now we need to check that the sender of this event + } + + // so far so good, but now we need to check that the sender of this event // hadn't advertised someone else's Curve25519 key as their own. We do that // by checking the Ed25519 claimed by the event (or, in the case of megolm, // the event which set up the megolm session), to check that it matches the @@ -2307,29 +1982,24 @@ // // (see https://github.com/vector-im/vector-web/issues/2215) - const claimedKey = event.getClaimedEd25519Key(); - if (!claimedKey) { _logger.logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device"); - return null; } - if (claimedKey !== device.getFingerprint()) { _logger.logger.warn("Event " + event.getId() + " claims ed25519 key " + claimedKey + " but sender device has key " + device.getFingerprint()); - return null; } - return device; } + /** * Get information about the encryption of an event * - * @param {module:models/event.MatrixEvent} event event to be checked + * @param event - event to be checked * - * @return {object} An object with the fields: + * @returns An object with the fields: * - encrypted: whether the event is encrypted (if not encrypted, some of the * other properties may not be set) * - senderKey: the sender's key @@ -2340,32 +2010,30 @@ * - mismatchedSender: if the event's ed25519 and curve25519 keys don't match * (only meaningful if `sender` is set) */ - - getEventEncryptionInfo(event) { const ret = {}; - ret.senderKey = event.getSenderKey(); + ret.senderKey = event.getSenderKey() ?? undefined; ret.algorithm = event.getWireContent().algorithm; - if (!ret.senderKey || !ret.algorithm) { ret.encrypted = false; return ret; } - ret.encrypted = true; - if (event.isKeySourceUntrusted()) { // we got the key this event from somewhere else // TODO: check if we can trust the forwarders. ret.authenticated = false; } else { ret.authenticated = true; - } // senderKey is the Curve25519 identity key of the device which the event + } + + // senderKey is the Curve25519 identity key of the device which the event // was sent from. In the case of Megolm, it's actually the Curve25519 // identity key of the device which set up the Megolm session. + ret.sender = this.deviceList.getDeviceByIdentityKey(ret.algorithm, ret.senderKey) ?? undefined; - ret.sender = this.deviceList.getDeviceByIdentityKey(ret.algorithm, ret.senderKey); // so far so good, but now we need to check that the sender of this event + // so far so good, but now we need to check that the sender of this event // hadn't advertised someone else's Curve25519 key as their own. We do that // by checking the Ed25519 claimed by the event (or, in the case of megolm, // the event which set up the megolm session), to check that it matches the @@ -2374,109 +2042,121 @@ // (see https://github.com/vector-im/vector-web/issues/2215) const claimedKey = event.getClaimedEd25519Key(); - if (!claimedKey) { _logger.logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device"); - ret.mismatchedSender = true; } - if (ret.sender && claimedKey !== ret.sender.getFingerprint()) { _logger.logger.warn("Event " + event.getId() + " claims ed25519 key " + claimedKey + "but sender device has key " + ret.sender.getFingerprint()); - ret.mismatchedSender = true; } - return ret; } + /** * Forces the current outbound group session to be discarded such * that another one will be created next time an event is sent. * - * @param {string} roomId The ID of the room to discard the session for + * @param roomId - The ID of the room to discard the session for * * This should not normally be necessary. */ - - forceDiscardSession(roomId) { const alg = this.roomEncryptors.get(roomId); if (alg === undefined) throw new Error("Room not encrypted"); - if (alg.forceDiscardSession === undefined) { throw new Error("Room encryption algorithm doesn't support session discarding"); } - alg.forceDiscardSession(); } + /** * Configure a room to use encryption (ie, save a flag in the cryptoStore). * - * @param {string} roomId The room ID to enable encryption in. + * @param roomId - The room ID to enable encryption in. * - * @param {object} config The encryption config for the room. + * @param config - The encryption config for the room. * - * @param {boolean=} inhibitDeviceQuery true to suppress device list query for + * @param inhibitDeviceQuery - true to suppress device list query for * users in the room (for now). In case lazy loading is enabled, * the device query is always inhibited as the members are not tracked. + * + * @deprecated It is normally incorrect to call this method directly. Encryption + * is enabled by receiving an `m.room.encryption` event (which we may have sent + * previously). */ + async setRoomEncryption(roomId, config, inhibitDeviceQuery) { + const room = this.clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to enable encryption tracking devices in unknown room ${roomId}`); + } + await this.setRoomEncryptionImpl(room, config); + if (!this.lazyLoadMembers && !inhibitDeviceQuery) { + this.deviceList.refreshOutdatedDeviceLists(); + } + } + /** + * Set up encryption for a room. + * + * This is called when an m.room.encryption event is received. It saves a flag + * for the room in the cryptoStore (if it wasn't already set), sets up an "encryptor" for + * the room, and enables device-list tracking for the room. + * + * It does not initiate a device list query for the room. That is normally + * done once we finish processing the sync, in onSyncCompleted. + * + * @param room - The room to enable encryption in. + * @param config - The encryption config for the room. + */ + async setRoomEncryptionImpl(room, config) { + const roomId = room.roomId; - async setRoomEncryption(roomId, config, inhibitDeviceQuery) { // ignore crypto events with no algorithm defined // This will happen if a crypto event is redacted before we fetch the room state // It would otherwise just throw later as an unknown algorithm would, but we may // as well catch this here if (!config.algorithm) { _logger.logger.log("Ignoring setRoomEncryption with no algorithm"); - return; - } // if state is being replayed from storage, we might already have a configuration + } + + // if state is being replayed from storage, we might already have a configuration // for this room as they are persisted as well. // We just need to make sure the algorithm is initialized in this case. // However, if the new config is different, // we should bail out as room encryption can't be changed once set. - - const existingConfig = this.roomList.getRoomEncryption(roomId); - if (existingConfig) { if (JSON.stringify(existingConfig) != JSON.stringify(config)) { _logger.logger.error("Ignoring m.room.encryption event which requests " + "a change of config in " + roomId); - return; } - } // if we already have encryption in this room, we should ignore this event, + } + // if we already have encryption in this room, we should ignore this event, // as it would reset the encryption algorithm. // This is at least expected to be called twice, as sync calls onCryptoEvent // for both the timeline and state sections in the /sync response, // the encryption event would appear in both. // If it's called more than twice though, // it signals a bug on client or server. - - const existingAlg = this.roomEncryptors.get(roomId); - if (existingAlg) { return; - } // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption + } + + // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption // because it first stores in memory. We should await the promise only // after all the in-memory state (roomEncryptors and _roomList) has been updated // to avoid races when calling this method multiple times. Hence keep a hold of the promise. - - let storeConfigPromise = null; - if (!existingConfig) { storeConfigPromise = this.roomList.setRoomEncryption(roomId, config); } - const AlgClass = algorithms.ENCRYPTION_CLASSES.get(config.algorithm); - if (!AlgClass) { throw new Error("Unable to encrypt with " + config.algorithm); } - const alg = new AlgClass({ userId: this.userId, deviceId: this.deviceId, @@ -2487,118 +2167,121 @@ config }); this.roomEncryptors.set(roomId, alg); - if (storeConfigPromise) { await storeConfigPromise; } + _logger.logger.log(`Enabling encryption in ${roomId}`); - if (!this.lazyLoadMembers) { - _logger.logger.log("Enabling encryption in " + roomId + "; " + "starting to track device lists for all users therein"); - - await this.trackRoomDevices(roomId); // TODO: this flag is only not used from MatrixClient::setRoomEncryption - // which is never used (inside Element at least) - // but didn't want to remove it as it technically would - // be a breaking change. - - if (!inhibitDeviceQuery) { - this.deviceList.refreshOutdatedDeviceLists(); - } + // we don't want to force a download of the full membership list of this room, but as soon as we have that + // list we can start tracking the device list. + if (room.membersLoaded()) { + await this.trackRoomDevicesImpl(room); } else { - _logger.logger.log("Enabling encryption in " + roomId); + // wait for the membership list to be loaded + const onState = _state => { + room.off(_roomState.RoomStateEvent.Update, onState); + if (room.membersLoaded()) { + this.trackRoomDevicesImpl(room).catch(e => { + _logger.logger.error(`Error enabling device tracking in ${roomId}`, e); + }); + } + }; + room.on(_roomState.RoomStateEvent.Update, onState); } } + /** * Make sure we are tracking the device lists for all users in this room. * - * @param {string} roomId The room ID to start tracking devices in. - * @returns {Promise} when all devices for the room have been fetched and marked to track + * @param roomId - The room ID to start tracking devices in. + * @returns when all devices for the room have been fetched and marked to track + * @deprecated there's normally no need to call this function: device list tracking + * will be enabled as soon as we have the full membership list. */ - - trackRoomDevices(roomId) { + const room = this.clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); + } + return this.trackRoomDevicesImpl(room); + } + + /** + * Make sure we are tracking the device lists for all users in this room. + * + * This is normally called when we are about to send an encrypted event, to make sure + * we have all the devices in the room; but it is also called when processing an + * m.room.encryption state event (if lazy-loading is disabled), or when members are + * loaded (if lazy-loading is enabled), to prepare the device list. + * + * @param room - Room to enable device-list tracking in + */ + trackRoomDevicesImpl(room) { + const roomId = room.roomId; const trackMembers = async () => { // not an encrypted room if (!this.roomEncryptors.has(roomId)) { return; } - - const room = this.clientStore.getRoom(roomId); - - if (!room) { - throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); - } - _logger.logger.log(`Starting to track devices for room ${roomId} ...`); - const members = await room.getEncryptionTargetMembers(); members.forEach(m => { this.deviceList.startTrackingDeviceList(m.userId); }); }; - let promise = this.roomDeviceTrackingState[roomId]; - if (!promise) { promise = trackMembers(); this.roomDeviceTrackingState[roomId] = promise.catch(err => { - this.roomDeviceTrackingState[roomId] = null; + delete this.roomDeviceTrackingState[roomId]; throw err; }); } - return promise; } + /** * Try to make sure we have established olm sessions for all known devices for * the given users. * - * @param {string[]} users list of user ids - * @param {boolean} force If true, force a new Olm session to be created. Default false. + * @param users - list of user ids + * @param force - If true, force a new Olm session to be created. Default false. * - * @return {Promise} resolves once the sessions are complete, to + * @returns resolves once the sessions are complete, to * an Object mapping from userId to deviceId to - * {@link module:crypto~OlmSessionResult} + * {@link OlmSessionResult} */ - - ensureOlmSessionsForUsers(users, force) { - const devicesByUser = {}; - - for (let i = 0; i < users.length; ++i) { - const userId = users[i]; - devicesByUser[userId] = []; + // map user Id → DeviceInfo[] + const devicesByUser = new Map(); + for (const userId of users) { + const userDevices = []; + devicesByUser.set(userId, userDevices); const devices = this.getStoredDevicesForUser(userId) || []; - - for (let j = 0; j < devices.length; ++j) { - const deviceInfo = devices[j]; + for (const deviceInfo of devices) { const key = deviceInfo.getIdentityKey(); - if (key == this.olmDevice.deviceCurve25519Key) { // don't bother setting up session to ourself continue; } - if (deviceInfo.verified == DeviceVerification.BLOCKED) { // don't bother setting up sessions with blocked users continue; } - - devicesByUser[userId].push(deviceInfo); + userDevices.push(deviceInfo); } } - return olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, force); } + /** * Get a list containing all of the room keys * - * @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects + * @returns a list of session export objects */ - - async exportRoomKeys() { const exportedSessions = []; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], txn => { this.cryptoStore.getAllEndToEndInboundGroupSessions(txn, s => { if (s === null) return; const sess = this.olmDevice.exportInboundGroupSession(s.senderKey, s.sessionId, s.sessionData); @@ -2609,164 +2292,144 @@ }); return exportedSessions; } + /** * Import a list of room keys previously exported by exportRoomKeys * - * @param {Object[]} keys a list of session export objects - * @param {Object} opts - * @param {Function} opts.progressCallback called with an object which has a stage param - * @return {Promise} a promise which resolves once the keys have been imported + * @param keys - a list of session export objects + * @returns a promise which resolves once the keys have been imported */ - - importRoomKeys(keys, opts = {}) { let successes = 0; let failures = 0; const total = keys.length; - function updateProgress() { - opts.progressCallback({ + opts.progressCallback?.({ stage: "load_keys", successes, failures, total }); } - return Promise.all(keys.map(key => { if (!key.room_id || !key.algorithm) { _logger.logger.warn("ignoring room key entry with missing fields", key); - failures++; - if (opts.progressCallback) { updateProgress(); } - return null; } - const alg = this.getRoomDecryptor(key.room_id, key.algorithm); return alg.importRoomKey(key, opts).finally(() => { successes++; - if (opts.progressCallback) { updateProgress(); } }); })).then(); } + /** * Counts the number of end to end session keys that are waiting to be backed up - * @returns {Promise} Resolves to the number of sessions requiring backup + * @returns Promise which resolves to the number of sessions requiring backup */ - - countSessionsNeedingBackup() { return this.backupManager.countSessionsNeedingBackup(); } + /** * Perform any background tasks that can be done before a message is ready to * send, in order to speed up sending of the message. * - * @param {module:models/room} room the room the event is in + * @param room - the room the event is in */ - - prepareToEncrypt(room) { const alg = this.roomEncryptors.get(room.roomId); - if (alg) { alg.prepareToEncrypt(room); } } + /** * Encrypt an event according to the configuration of the room. * - * @param {module:models/event.MatrixEvent} event event to be sent + * @param event - event to be sent * - * @param {module:models/room} room destination room. + * @param room - destination room. * - * @return {Promise?} Promise which resolves when the event has been + * @returns Promise which resolves when the event has been * encrypted, or null if nothing was needed */ - - async encryptEvent(event, room) { - if (!room) { - throw new Error("Cannot send encrypted messages in unknown rooms"); - } - const roomId = event.getRoomId(); const alg = this.roomEncryptors.get(roomId); - if (!alg) { // MatrixClient has already checked that this room should be encrypted, // so this is an unexpected situation. - throw new Error("Room was previously configured to use encryption, but is " + "no longer. Perhaps the homeserver is hiding the " + "configuration event."); + throw new Error("Room " + roomId + " was previously configured to use encryption, but is " + "no longer. Perhaps the homeserver is hiding the " + "configuration event."); } - if (!this.roomDeviceTrackingState[roomId]) { - this.trackRoomDevices(roomId); - } // wait for all the room devices to be loaded - - - await this.roomDeviceTrackingState[roomId]; - let content = event.getContent(); // If event has an m.relates_to then we need + // wait for all the room devices to be loaded + await this.trackRoomDevicesImpl(room); + let content = event.getContent(); + // If event has an m.relates_to then we need // to put this on the wrapping event instead - - const mRelatesTo = content['m.relates_to']; - + const mRelatesTo = content["m.relates_to"]; if (mRelatesTo) { // Clone content here so we don't remove `m.relates_to` from the local-echo content = Object.assign({}, content); - delete content['m.relates_to']; - } // Treat element's performance metrics the same as `m.relates_to` (when present) - - - const elementPerfMetrics = content['io.element.performance_metrics']; + delete content["m.relates_to"]; + } + // Treat element's performance metrics the same as `m.relates_to` (when present) + const elementPerfMetrics = content["io.element.performance_metrics"]; if (elementPerfMetrics) { content = Object.assign({}, content); - delete content['io.element.performance_metrics']; + delete content["io.element.performance_metrics"]; } - const encryptedContent = await alg.encryptMessage(room, event.getType(), content); - if (mRelatesTo) { - encryptedContent['m.relates_to'] = mRelatesTo; + encryptedContent["m.relates_to"] = mRelatesTo; } - if (elementPerfMetrics) { - encryptedContent['io.element.performance_metrics'] = elementPerfMetrics; + encryptedContent["io.element.performance_metrics"] = elementPerfMetrics; } - event.makeEncrypted("m.room.encrypted", encryptedContent, this.olmDevice.deviceCurve25519Key, this.olmDevice.deviceEd25519Key); } + /** * Decrypt a received event * - * @param {MatrixEvent} event * - * @return {Promise} resolves once we have + * @returns resolves once we have * finished decrypting. Rejects with an `algorithms.DecryptionError` if there * is a problem decrypting the event. */ - - async decryptEvent(event) { if (event.isRedacted()) { + // Try to decrypt the redaction event, to support encrypted + // redaction reasons. If we can't decrypt, just fall back to using + // the original redacted_because. const redactionEvent = new _event2.MatrixEvent(_objectSpread({ room_id: event.getRoomId() }, event.getUnsigned().redacted_because)); - const decryptedEvent = await this.decryptEvent(redactionEvent); + let redactedBecause = event.getUnsigned().redacted_because; + if (redactionEvent.isEncrypted()) { + try { + const decryptedEvent = await this.decryptEvent(redactionEvent); + redactedBecause = decryptedEvent.clearEvent; + } catch (e) { + _logger.logger.warn("Decryption of redaction failed. Falling back to unencrypted event.", e); + } + } return { clearEvent: { room_id: event.getRoomId(), type: "m.room.message", content: {}, unsigned: { - redacted_because: decryptedEvent.clearEvent + redacted_because: redactedBecause } } }; @@ -2776,20 +2439,21 @@ return alg.decryptEvent(event); } } + /** * Handle the notification from /sync or /keys/changes that device lists have * been changed. * - * @param {Object} syncData Object containing sync tokens associated with this sync - * @param {Object} syncDeviceLists device_lists field from /sync, or response from + * @param syncData - Object containing sync tokens associated with this sync + * @param syncDeviceLists - device_lists field from /sync, or response from * /keys/changes */ - - async handleDeviceListChanges(syncData, syncDeviceLists) { // Initial syncs don't have device change lists. We'll either get the complete list // of changes for the interval or will have invalidated everything in willProcessSync - if (!syncData.oldSyncToken) return; // Here, we're relying on the fact that we only ever save the sync data after + if (!syncData.oldSyncToken) return; + + // Here, we're relying on the fact that we only ever save the sync data after // sucessfully saving the device list data, so we're guaranteed that the device // list store is at least as fresh as the sync token from the sync store, ie. // any device changes received in sync tokens prior to the 'next' token here @@ -2797,21 +2461,17 @@ // If we didn't make this assumption, we'd have to use the /keys/changes API // to get key changes between the sync token in the device list and the 'old' // sync token used here to make sure we didn't miss any. - await this.evalDeviceListChanges(syncDeviceLists); } + /** * Send a request for some room keys, if we have not already done so * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * @param {Array<{userId: string, deviceId: string}>} recipients - * @param {boolean} resend whether to resend the key request if there is + * @param resend - whether to resend the key request if there is * already one * - * @return {Promise} a promise that resolves when the key request is queued + * @returns a promise that resolves when the key request is queued */ - - requestRoomKey(requestBody, recipients, resend = false) { return this.outgoingRoomKeyRequestManager.queueRoomKeyRequest(requestBody, recipients, resend).then(() => { if (this.sendKeyRequestsImmediately) { @@ -2819,57 +2479,45 @@ } }).catch(e => { // this normally means we couldn't talk to the store - _logger.logger.error('Error requesting key for event', e); + _logger.logger.error("Error requesting key for event", e); }); } + /** * Cancel any earlier room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * parameters to match for cancellation + * @param requestBody - parameters to match for cancellation */ - - cancelRoomKeyRequest(requestBody) { this.outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody).catch(e => { _logger.logger.warn("Error clearing pending room key requests", e); }); } + /** * Re-send any outgoing key requests, eg after verification - * @returns {Promise} + * @returns */ - - async cancelAndResendAllOutgoingKeyRequests() { await this.outgoingRoomKeyRequestManager.cancelAndResendAllOutgoingRequests(); } + /** * handle an m.room.encryption event * - * @param {module:models/event.MatrixEvent} event encryption event + * @param room - in which the event was received + * @param event - encryption event to be processed */ - - - async onCryptoEvent(event) { - const roomId = event.getRoomId(); + async onCryptoEvent(room, event) { const content = event.getContent(); - - try { - // inhibit the device list refresh for now - it will happen once we've - // finished processing the sync, in onSyncCompleted. - await this.setRoomEncryption(roomId, content, true); - } catch (e) { - _logger.logger.error("Error configuring encryption in room " + roomId + ":", e); - } + await this.setRoomEncryptionImpl(room, content); } + /** * Called before the result of a sync is processed * - * @param {Object} syncData the data from the 'MatrixClient.sync' event + * @param syncData - the data from the 'MatrixClient.sync' event */ - - async onSyncWillProcess(syncData) { if (!syncData.oldSyncToken) { // If there is no old sync token, we start all our tracking from @@ -2877,63 +2525,62 @@ // be called for all e2e rooms during the processing of the sync, // at which point we'll start tracking all the users of that room. _logger.logger.log("Initial sync performed - resetting device tracking state"); - - this.deviceList.stopTrackingAllDeviceLists(); // we always track our own device list (for key backups etc) - + this.deviceList.stopTrackingAllDeviceLists(); + // we always track our own device list (for key backups etc) this.deviceList.startTrackingDeviceList(this.userId); this.roomDeviceTrackingState = {}; } - this.sendKeyRequestsImmediately = false; } + /** * handle the completion of a /sync * * This is called after the processing of each successful /sync response. * It is an opportunity to do a batch process on the information received. * - * @param {Object} syncData the data from the 'MatrixClient.sync' event + * @param syncData - the data from the 'MatrixClient.sync' event */ - - async onSyncCompleted(syncData) { - this.deviceList.setSyncToken(syncData.nextSyncToken); - this.deviceList.saveIfDirty(); // we always track our own device list (for key backups etc) + this.deviceList.setSyncToken(syncData.nextSyncToken ?? null); + this.deviceList.saveIfDirty(); + // we always track our own device list (for key backups etc) this.deviceList.startTrackingDeviceList(this.userId); - this.deviceList.refreshOutdatedDeviceLists(); // we don't start uploading one-time keys until we've caught up with + this.deviceList.refreshOutdatedDeviceLists(); + + // we don't start uploading one-time keys until we've caught up with // to-device messages, to help us avoid throwing away one-time-keys that we // are about to receive messages for // (https://github.com/vector-im/element-web/issues/2782). - if (!syncData.catchingUp) { this.maybeUploadOneTimeKeys(); - this.processReceivedRoomKeyRequests(); // likewise don't start requesting keys until we've caught up + this.processReceivedRoomKeyRequests(); + + // likewise don't start requesting keys until we've caught up // on to_device messages, otherwise we'll request keys that we're // just about to get. + this.outgoingRoomKeyRequestManager.sendQueuedRequests(); - this.outgoingRoomKeyRequestManager.sendQueuedRequests(); // Sync has finished so send key requests straight away. - + // Sync has finished so send key requests straight away. this.sendKeyRequestsImmediately = true; } } + /** * Trigger the appropriate invalidations and removes for a given * device list * - * @param {Object} deviceLists device_lists field from /sync, or response from + * @param deviceLists - device_lists field from /sync, or response from * /keys/changes */ - - async evalDeviceListChanges(deviceLists) { - if (deviceLists.changed && Array.isArray(deviceLists.changed)) { + if (Array.isArray(deviceLists?.changed)) { deviceLists.changed.forEach(u => { this.deviceList.invalidateUserDeviceList(u); }); } - - if (deviceLists.left && Array.isArray(deviceLists.left) && deviceLists.left.length) { + if (Array.isArray(deviceLists?.left) && deviceLists.left.length) { // Check we really don't share any rooms with these users // any more: the server isn't required to give us the // exact correct set. @@ -2945,71 +2592,62 @@ }); } } + /** * Get a list of all the IDs of users we share an e2e room with * for which we are tracking devices already * - * @returns {string[]} List of user IDs + * @returns List of user IDs */ - - async getTrackedE2eUsers() { const e2eUserIds = []; - for (const room of this.getTrackedE2eRooms()) { const members = await room.getEncryptionTargetMembers(); - for (const member of members) { e2eUserIds.push(member.userId); } } - return e2eUserIds; } + /** * Get a list of the e2e-enabled rooms we are members of, * and for which we are already tracking the devices * - * @returns {module:models.Room[]} + * @returns */ - - getTrackedE2eRooms() { return this.clientStore.getRooms().filter(room => { // check for rooms with encryption enabled const alg = this.roomEncryptors.get(room.roomId); - if (!alg) { return false; } - if (!this.roomDeviceTrackingState[room.roomId]) { return false; - } // ignore any rooms which we have left - + } + // ignore any rooms which we have left const myMembership = room.getMyMembership(); return myMembership === "join" || myMembership === "invite"; }); } + /** * Encrypts and sends a given object via Olm to-device messages to a given * set of devices. - * @param {object[]} userDeviceInfoArr the devices to send to - * @param {object} payload fields to include in the encrypted payload - * @return {Promise<{contentMap, deviceInfoByDeviceId}>} Promise which + * @param userDeviceInfoArr - the devices to send to + * @param payload - fields to include in the encrypted payload + * @returns Promise which * resolves once the message has been encrypted and sent to the given - * userDeviceMap, and returns the { contentMap, deviceInfoByDeviceId } + * userDeviceMap, and returns the `{ contentMap, deviceInfoByDeviceId }` * of the successfully sent messages. */ - - async encryptAndSendToDevices(userDeviceInfoArr, payload) { const toDeviceBatch = { eventType: _event.EventType.RoomMessageEncrypted, batch: [] }; - try { await Promise.all(userDeviceInfoArr.map(async ({ userId, @@ -3019,317 +2657,283 @@ const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, - ciphertext: {} + ciphertext: {}, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() }; toDeviceBatch.batch.push({ userId, deviceId, payload: encryptedContent }); - await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, { - [userId]: [deviceInfo] - }); + await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, new Map([[userId, [deviceInfo]]])); await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this.userId, this.deviceId, this.olmDevice, userId, deviceInfo, payload); - })); // prune out any devices that encryptMessageForDevice could not encrypt for, + })); + + // prune out any devices that encryptMessageForDevice could not encrypt for, // in which case it will have just not added anything to the ciphertext object. // There's no point sending messages to devices if we couldn't encrypt to them, // since that's effectively a blank message. - toDeviceBatch.batch = toDeviceBatch.batch.filter(msg => { if (Object.keys(msg.payload.ciphertext).length > 0) { return true; } else { _logger.logger.log(`No ciphertext for device ${msg.userId}:${msg.deviceId}: pruning`); - return false; } }); - try { await this.baseApis.queueToDevice(toDeviceBatch); } catch (e) { _logger.logger.error("sendToDevice failed", e); - throw e; } } catch (e) { _logger.logger.error("encryptAndSendToDevices promises failed", e); - throw e; } } - + async preprocessToDeviceMessages(events) { + // all we do here is filter out encrypted to-device messages with the wrong algorithm. Decryption + // happens later in decryptEvent, via the EventMapper + return events.filter(toDevice => { + if (toDevice.type === _event.EventType.RoomMessageEncrypted && !["m.olm.v1.curve25519-aes-sha2"].includes(toDevice.content?.algorithm)) { + _logger.logger.log("Ignoring invalid encrypted to-device event from " + toDevice.sender); + return false; + } + return true; + }); + } /** * Handle a key event * - * @private - * @param {module:models/event.MatrixEvent} event key event + * @internal + * @param event - key event */ onRoomKeyEvent(event) { const content = event.getContent(); - if (!content.room_id || !content.algorithm) { _logger.logger.error("key event is missing fields"); - return; } - if (!this.backupManager.checkedForBackup) { // don't bother awaiting on this - the important thing is that we retry if we // haven't managed to check before this.backupManager.checkAndStart(); } - const alg = this.getRoomDecryptor(content.room_id, content.algorithm); alg.onRoomKeyEvent(event); } + /** * Handle a key withheld event * - * @private - * @param {module:models/event.MatrixEvent} event key withheld event + * @internal + * @param event - key withheld event */ - - onRoomKeyWithheldEvent(event) { const content = event.getContent(); - if (content.code !== "m.no_olm" && (!content.room_id || !content.session_id) || !content.algorithm || !content.sender_key) { _logger.logger.error("key withheld event is missing fields"); - return; } - - _logger.logger.info(`Got room key withheld event from ${event.getSender()} (${content.sender_key}) ` + `for ${content.algorithm}/${content.room_id}/${content.session_id} ` + `with reason ${content.code} (${content.reason})`); - + _logger.logger.info(`Got room key withheld event from ${event.getSender()} ` + `for ${content.algorithm} session ${content.sender_key}|${content.session_id} ` + `in room ${content.room_id} with code ${content.code} (${content.reason})`); const alg = this.getRoomDecryptor(content.room_id, content.algorithm); - if (alg.onRoomKeyWithheldEvent) { alg.onRoomKeyWithheldEvent(event); } - if (!content.room_id) { // retry decryption for all events sent by the sender_key. This will // update the events to show a message indicating that the olm session was // wedged. const roomDecryptors = this.getRoomDecryptors(content.algorithm); - for (const decryptor of roomDecryptors) { decryptor.retryDecryptionFromSender(content.sender_key); } } } + /** * Handle a general key verification event. * - * @private - * @param {module:models/event.MatrixEvent} event verification start event + * @internal + * @param event - verification start event */ - - onKeyVerificationMessage(event) { if (!_ToDeviceChannel.ToDeviceChannel.validateEvent(event, this.baseApis)) { return; } - const createRequest = event => { if (!_ToDeviceChannel.ToDeviceChannel.canCreateRequest(_ToDeviceChannel.ToDeviceChannel.getEventType(event))) { return; } - const content = event.getContent(); const deviceId = content && content.from_device; - if (!deviceId) { return; } - const userId = event.getSender(); const channel = new _ToDeviceChannel.ToDeviceChannel(this.baseApis, userId, [deviceId]); return new _VerificationRequest.VerificationRequest(channel, this.verificationMethods, this.baseApis); }; - this.handleVerificationEvent(event, this.toDeviceVerificationRequests, createRequest); } + /** * Handle key verification requests sent as timeline events * - * @private - * @param {module:models/event.MatrixEvent} event the timeline event - * @param {module:models/Room} room not used - * @param {boolean} atStart not used - * @param {boolean} removed not used - * @param {boolean} { liveEvent } whether this is a live event + * @internal + * @param event - the timeline event + * @param room - not used + * @param atStart - not used + * @param removed - not used + * @param whether - this is a live event */ - async handleVerificationEvent(event, requestsMap, createRequest, isLiveEvent = true) { // Wait for event to get its final ID with pendingEventOrdering: "chronological", since DM channels depend on it. if (event.isSending() && event.status != _event2.EventStatus.SENT) { let eventIdListener; let statusListener; - try { await new Promise((resolve, reject) => { eventIdListener = resolve; - statusListener = () => { if (event.status == _event2.EventStatus.CANCELLED) { reject(new Error("Event status set to CANCELLED.")); } }; - event.once(_event2.MatrixEventEvent.LocalEventIdReplaced, eventIdListener); event.on(_event2.MatrixEventEvent.Status, statusListener); }); } catch (err) { _logger.logger.error("error while waiting for the verification event to be sent: ", err); - return; } finally { event.removeListener(_event2.MatrixEventEvent.LocalEventIdReplaced, eventIdListener); event.removeListener(_event2.MatrixEventEvent.Status, statusListener); } } - let request = requestsMap.getRequest(event); let isNewRequest = false; - if (!request) { - request = createRequest(event); // a request could not be made from this event, so ignore event - + request = createRequest(event); + // a request could not be made from this event, so ignore event if (!request) { _logger.logger.log(`Crypto: could not find VerificationRequest for ` + `${event.getType()}, and could not create one, so ignoring.`); - return; } - isNewRequest = true; requestsMap.setRequest(event, request); } - event.setVerificationRequest(request); - try { await request.channel.handleEvent(event, request, isLiveEvent); } catch (err) { _logger.logger.error("error while handling verification event", err); } - - const shouldEmit = isNewRequest && !request.initiatedByMe && !request.invalid && // check it has enough events to pass the UNSENT stage + const shouldEmit = isNewRequest && !request.initiatedByMe && !request.invalid && + // check it has enough events to pass the UNSENT stage !request.observeOnly; - if (shouldEmit) { this.baseApis.emit(CryptoEvent.VerificationRequest, request); } } + /** * Handle a toDevice event that couldn't be decrypted * - * @private - * @param {module:models/event.MatrixEvent} event undecryptable event + * @internal + * @param event - undecryptable event */ - - async onToDeviceBadEncrypted(event) { const content = event.getWireContent(); const sender = event.getSender(); const algorithm = content.algorithm; - const deviceKey = content.sender_key; // retry decryption for all events sent by the sender_key. This will + const deviceKey = content.sender_key; + this.baseApis.emit(_client.ClientEvent.UndecryptableToDeviceEvent, event); + + // retry decryption for all events sent by the sender_key. This will // update the events to show a message indicating that the olm session was // wedged. - const retryDecryption = () => { const roomDecryptors = this.getRoomDecryptors(olmlib.MEGOLM_ALGORITHM); - for (const decryptor of roomDecryptors) { decryptor.retryDecryptionFromSender(deviceKey); } }; - if (sender === undefined || deviceKey === undefined || deviceKey === undefined) { return; - } // check when we last forced a new session with this device: if we've already done so - // recently, don't do it again. - - - this.lastNewSessionForced[sender] = this.lastNewSessionForced[sender] || {}; - const lastNewSessionForced = this.lastNewSessionForced[sender][deviceKey] || 0; + } + // check when we last forced a new session with this device: if we've already done so + // recently, don't do it again. + const lastNewSessionDevices = this.lastNewSessionForced.getOrCreate(sender); + const lastNewSessionForced = lastNewSessionDevices.getOrCreate(deviceKey); if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) { _logger.logger.debug("New session already forced with device " + sender + ":" + deviceKey + " at " + lastNewSessionForced + ": not forcing another"); - await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true); retryDecryption(); return; - } // establish a new olm session with this device since we're failing to decrypt messages + } + + // establish a new olm session with this device since we're failing to decrypt messages // on a current session. // Note that an undecryptable message from another device could easily be spoofed - // is there anything we can do to mitigate this? - - let device = this.deviceList.getDeviceByIdentityKey(algorithm, deviceKey); - if (!device) { // if we don't know about the device, fetch the user's devices again // and retry before giving up await this.downloadKeys([sender], false); device = this.deviceList.getDeviceByIdentityKey(algorithm, deviceKey); - if (!device) { _logger.logger.info("Couldn't find device for identity key " + deviceKey + ": not re-establishing session"); - await this.olmDevice.recordSessionProblem(deviceKey, "wedged", false); retryDecryption(); return; } } - - const devicesByUser = {}; - devicesByUser[sender] = [device]; + const devicesByUser = new Map([[sender, [device]]]); await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser, true); - this.lastNewSessionForced[sender][deviceKey] = Date.now(); // Now send a blank message on that session so the other side knows about it. + lastNewSessionDevices.set(deviceKey, Date.now()); + + // Now send a blank message on that session so the other side knows about it. // (The keyshare request is sent in the clear so that won't do) // We send this first such that, as long as the toDevice messages arrive in the // same order we sent them, the other end will get this first, set up the new session, // then get the keyshare request and send the key over this new session (because it // is the session it has most recently received a message on). - const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.olmDevice.deviceCurve25519Key, - ciphertext: {} + ciphertext: {}, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() }; await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this.userId, this.deviceId, this.olmDevice, sender, device, { type: "m.dummy" }); await this.olmDevice.recordSessionProblem(deviceKey, "wedged", true); retryDecryption(); - await this.baseApis.sendToDevice("m.room.encrypted", { - [sender]: { - [device.deviceId]: encryptedContent - } - }); // Most of the time this probably won't be necessary since we'll have queued up a key request when + await this.baseApis.sendToDevice("m.room.encrypted", new Map([[sender, new Map([[device.deviceId, encryptedContent]])]])); + + // Most of the time this probably won't be necessary since we'll have queued up a key request when // we failed to decrypt the message and will be waiting a bit for the key to arrive before sending // it. This won't always be the case though so we need to re-send any that have already been sent // to avoid races. - const requestsToResend = await this.outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(sender, device.deviceId); - for (const keyReq of requestsToResend) { this.requestRoomKey(keyReq.requestBody, keyReq.recipients, true); } } + /** * Handle a change in the membership state of a member of a room * - * @private - * @param {module:models/event.MatrixEvent} event event causing the change - * @param {module:models/room-member} member user whose membership changed - * @param {string=} oldMembership previous membership + * @internal + * @param event - event causing the change + * @param member - user whose membership changed + * @param oldMembership - previous membership */ - - onRoomMembership(event, member, oldMembership) { // this event handler is registered on the *client* (as opposed to the room // member itself), which means it is only called on changes to the *live* @@ -3338,44 +2942,38 @@ // // Further, it is automatically registered and called when new members // arrive in the room. + const roomId = member.roomId; const alg = this.roomEncryptors.get(roomId); - if (!alg) { // not encrypting in this room return; - } // only mark users in this room as tracked if we already started tracking in this room + } + // only mark users in this room as tracked if we already started tracking in this room // this way we don't start device queries after sync on behalf of this room which we won't use // the result of anyway, as we'll need to do a query again once all the members are fetched // by calling _trackRoomDevices - - - if (this.roomDeviceTrackingState[roomId]) { - if (member.membership == 'join') { - _logger.logger.log('Join event for ' + member.userId + ' in ' + roomId); // make sure we are tracking the deviceList for this user - - + if (roomId in this.roomDeviceTrackingState) { + if (member.membership == "join") { + _logger.logger.log("Join event for " + member.userId + " in " + roomId); + // make sure we are tracking the deviceList for this user this.deviceList.startTrackingDeviceList(member.userId); - } else if (member.membership == 'invite' && this.clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { - _logger.logger.log('Invite event for ' + member.userId + ' in ' + roomId); - + } else if (member.membership == "invite" && this.clientStore.getRoom(roomId)?.shouldEncryptForInvitedMembers()) { + _logger.logger.log("Invite event for " + member.userId + " in " + roomId); this.deviceList.startTrackingDeviceList(member.userId); } } - alg.onRoomMembership(event, member, oldMembership); } + /** * Called when we get an m.room_key_request event. * - * @private - * @param {module:models/event.MatrixEvent} event key request event + * @internal + * @param event - key request event */ - - onRoomKeyRequestEvent(event) { const content = event.getContent(); - if (content.action === "request") { // Queue it up for now, because they tend to arrive before the room state // events at initial sync, and we want to see if we know anything about the @@ -3387,37 +2985,35 @@ this.receivedRoomKeyRequestCancellations.push(req); } } + /** * Process any m.room_key_request events which were queued up during the * current sync. * - * @private + * @internal */ - - async processReceivedRoomKeyRequests() { if (this.processingRoomKeyRequests) { // we're still processing last time's requests; keep queuing new ones // up for now. return; } - this.processingRoomKeyRequests = true; - try { // we need to grab and clear the queues in the synchronous bit of this method, // so that we don't end up racing with the next /sync. const requests = this.receivedRoomKeyRequests; this.receivedRoomKeyRequests = []; const cancellations = this.receivedRoomKeyRequestCancellations; - this.receivedRoomKeyRequestCancellations = []; // Process all of the requests, *then* all of the cancellations. + this.receivedRoomKeyRequestCancellations = []; + + // Process all of the requests, *then* all of the cancellations. // // This makes sure that if we get a request and its cancellation in the // same /sync result, then we process the request before the // cancellation (and end up with a cancelled request), rather than the // cancellation before the request (and end up with an outstanding // request which should have been cancelled.) - await Promise.all(requests.map(req => this.processReceivedRoomKeyRequest(req))); await Promise.all(cancellations.map(cancellation => this.processReceivedRoomKeyRequestCancellation(cancellation))); } catch (e) { @@ -3426,47 +3022,36 @@ this.processingRoomKeyRequests = false; } } + /** * Helper for processReceivedRoomKeyRequests * - * @param {IncomingRoomKeyRequest} req */ - - async processReceivedRoomKeyRequest(req) { const userId = req.userId; const deviceId = req.deviceId; const body = req.requestBody; const roomId = body.room_id; const alg = body.algorithm; - _logger.logger.log(`m.room_key_request from ${userId}:${deviceId}` + ` for ${roomId} / ${body.session_id} (id ${req.requestId})`); - if (userId !== this.userId) { if (!this.roomEncryptors.get(roomId)) { _logger.logger.debug(`room key request for unencrypted room ${roomId}`); - return; } - const encryptor = this.roomEncryptors.get(roomId); const device = this.deviceList.getStoredDevice(userId, deviceId); - if (!device) { _logger.logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`); - return; } - try { await encryptor.reshareKeyWithDevice(body.sender_key, body.session_id, userId, device); } catch (e) { _logger.logger.warn("Failed to re-share keys for session " + body.session_id + " with device " + userId + ":" + device.deviceId, e); } - return; } - if (deviceId === this.deviceId) { // We'll always get these because we send room key requests to // '*' (ie. 'all devices') which includes the sending device, @@ -3477,162 +3062,134 @@ // always happen, but let's log anyway for now just in case it // causes issues. _logger.logger.log("Ignoring room key request from ourselves"); - return; - } // todo: should we queue up requests we don't yet have keys for, + } + + // todo: should we queue up requests we don't yet have keys for, // in case they turn up later? + // if we don't have a decryptor for this room/alg, we don't have // the keys for the requested events, and can drop the requests. - - if (!this.roomDecryptors.has(roomId)) { _logger.logger.log(`room key request for unencrypted room ${roomId}`); - return; } - const decryptor = this.roomDecryptors.get(roomId).get(alg); - if (!decryptor) { _logger.logger.log(`room key request for unknown alg ${alg} in room ${roomId}`); - return; } - if (!(await decryptor.hasKeysForKeyRequest(req))) { _logger.logger.log(`room key request for unknown session ${roomId} / ` + body.session_id); - return; } - req.share = () => { decryptor.shareKeysWithDevice(req); - }; // if the device is verified already, share the keys - + }; + // if the device is verified already, share the keys if (this.checkDeviceTrust(userId, deviceId).isVerified()) { - _logger.logger.log('device is already verified: sharing keys'); - + _logger.logger.log("device is already verified: sharing keys"); req.share(); return; } - this.emit(CryptoEvent.RoomKeyRequest, req); } + /** * Helper for processReceivedRoomKeyRequests * - * @param {IncomingRoomKeyRequestCancellation} cancellation */ - - async processReceivedRoomKeyRequestCancellation(cancellation) { - _logger.logger.log(`m.room_key_request cancellation for ${cancellation.userId}:` + `${cancellation.deviceId} (id ${cancellation.requestId})`); // we should probably only notify the app of cancellations we told it + _logger.logger.log(`m.room_key_request cancellation for ${cancellation.userId}:` + `${cancellation.deviceId} (id ${cancellation.requestId})`); + + // we should probably only notify the app of cancellations we told it // about, but we don't currently have a record of that, so we just pass // everything through. - - this.emit(CryptoEvent.RoomKeyRequestCancellation, cancellation); } + /** * Get a decryptor for a given room and algorithm. * * If we already have a decryptor for the given room and algorithm, return * it. Otherwise try to instantiate it. * - * @private + * @internal * - * @param {string?} roomId room id for decryptor. If undefined, a temporary + * @param roomId - room id for decryptor. If undefined, a temporary * decryptor is instantiated. * - * @param {string} algorithm crypto algorithm - * - * @return {module:crypto.algorithms.base.DecryptionAlgorithm} + * @param algorithm - crypto algorithm * - * @raises {module:crypto.algorithms.DecryptionError} if the algorithm is - * unknown + * @throws {@link DecryptionError} if the algorithm is unknown */ - - getRoomDecryptor(roomId, algorithm) { let decryptors; let alg; - roomId = roomId || null; - if (roomId) { decryptors = this.roomDecryptors.get(roomId); - if (!decryptors) { decryptors = new Map(); this.roomDecryptors.set(roomId, decryptors); } - alg = decryptors.get(algorithm); - if (alg) { return alg; } } - const AlgClass = algorithms.DECRYPTION_CLASSES.get(algorithm); - if (!AlgClass) { - throw new algorithms.DecryptionError('UNKNOWN_ENCRYPTION_ALGORITHM', 'Unknown encryption algorithm "' + algorithm + '".'); + throw new algorithms.DecryptionError("UNKNOWN_ENCRYPTION_ALGORITHM", 'Unknown encryption algorithm "' + algorithm + '".'); } - alg = new AlgClass({ userId: this.userId, crypto: this, olmDevice: this.olmDevice, baseApis: this.baseApis, - roomId: roomId + roomId: roomId ?? undefined }); - if (decryptors) { decryptors.set(algorithm, alg); } - return alg; } + /** * Get all the room decryptors for a given encryption algorithm. * - * @param {string} algorithm The encryption algorithm + * @param algorithm - The encryption algorithm * - * @return {array} An array of room decryptors + * @returns An array of room decryptors */ - - getRoomDecryptors(algorithm) { const decryptors = []; - for (const d of this.roomDecryptors.values()) { if (d.has(algorithm)) { decryptors.push(d.get(algorithm)); } } - return decryptors; } + /** * sign the given object with our ed25519 key * - * @param {Object} obj Object to which we will add a 'signatures' property + * @param obj - Object to which we will add a 'signatures' property */ - - async signObject(obj) { - const sigs = obj.signatures || {}; + const sigs = new Map(Object.entries(obj.signatures || {})); const unsigned = obj.unsigned; delete obj.signatures; delete obj.unsigned; - sigs[this.userId] = sigs[this.userId] || {}; - sigs[this.userId]["ed25519:" + this.deviceId] = await this.olmDevice.sign(_anotherJson.default.stringify(obj)); - obj.signatures = sigs; + const userSignatures = sigs.get(this.userId) || {}; + sigs.set(this.userId, userSignatures); + userSignatures["ed25519:" + this.deviceId] = await this.olmDevice.sign(_anotherJson.default.stringify(obj)); + obj.signatures = (0, _utils.recursiveMapToObject)(sigs); if (unsigned !== undefined) obj.unsigned = unsigned; } - } + /** * Fix up the backup key, that may be in the wrong format due to a bug in a * migration step. Some backup keys were stored as a comma-separated list of @@ -3640,133 +3197,73 @@ * passed a string that looks like a list of integers rather than a base64 * string, it will attempt to convert it to the right format. * - * @param {string} key the key to check - * @returns {null | string} If the key is in the wrong format, then the fixed + * @param key - the key to check + * @returns If the key is in the wrong format, then the fixed * key will be returned. Otherwise null will be returned. * */ - - exports.Crypto = Crypto; - function fixBackupKey(key) { if (typeof key !== "string" || key.indexOf(",") < 0) { return null; } - const fixedKey = Uint8Array.from(key.split(","), x => parseInt(x)); return olmlib.encodeBase64(fixedKey); } -/** - * The parameters of a room key request. The details of the request may - * vary with the crypto algorithm, but the management and storage layers for - * outgoing requests expect it to have 'room_id' and 'session_id' properties. - * - * @typedef {Object} RoomKeyRequestBody - */ /** * Represents a received m.room_key_request event - * - * @property {string} userId user requesting the key - * @property {string} deviceId device requesting the key - * @property {string} requestId unique id for the request - * @property {module:crypto~RoomKeyRequestBody} requestBody - * @property {function()} share callback which, when called, will ask - * the relevant crypto algorithm implementation to share the keys for - * this request. */ +class IncomingRoomKeyRequest { + /** user requesting the key */ + /** device requesting the key */ + + /** unique id for the request */ + + /** + * callback which, when called, will ask + * the relevant crypto algorithm implementation to share the keys for + * this request. + */ -class IncomingRoomKeyRequest { constructor(event) { _defineProperty(this, "userId", void 0); - _defineProperty(this, "deviceId", void 0); - _defineProperty(this, "requestId", void 0); - _defineProperty(this, "requestBody", void 0); - _defineProperty(this, "share", void 0); - const content = event.getContent(); this.userId = event.getSender(); this.deviceId = content.requesting_device_id; this.requestId = content.request_id; this.requestBody = content.body || {}; - this.share = () => { throw new Error("don't know how to share keys for this request yet"); }; } - } + /** * Represents a received m.room_key_request cancellation - * - * @property {string} userId user requesting the cancellation - * @property {string} deviceId device requesting the cancellation - * @property {string} requestId unique id for the request to be cancelled */ +exports.IncomingRoomKeyRequest = IncomingRoomKeyRequest; +class IncomingRoomKeyRequestCancellation { + /** user requesting the cancellation */ + /** device requesting the cancellation */ -exports.IncomingRoomKeyRequest = IncomingRoomKeyRequest; + /** unique id for the request to be cancelled */ -class IncomingRoomKeyRequestCancellation { constructor(event) { _defineProperty(this, "userId", void 0); - _defineProperty(this, "deviceId", void 0); - _defineProperty(this, "requestId", void 0); - const content = event.getContent(); this.userId = event.getSender(); this.deviceId = content.requesting_device_id; this.requestId = content.request_id; } - } -/** - * The result of a (successful) call to decryptEvent. - * - * @typedef {Object} EventDecryptionResult - * - * @property {Object} clearEvent The plaintext payload for the event - * (typically containing type and content fields). - * - * @property {?string} senderCurve25519Key Key owned by the sender of this - * event. See {@link module:models/event.MatrixEvent#getSenderKey}. - * - * @property {?string} claimedEd25519Key ed25519 key claimed by the sender of - * this event. See - * {@link module:models/event.MatrixEvent#getClaimedEd25519Key}. - * - * @property {?Array} forwardingCurve25519KeyChain list of curve25519 - * keys involved in telling us about the senderCurve25519Key and - * claimedEd25519Key. See - * {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}. - */ - -/** - * Fires when we receive a room key request - * - * @event module:client~MatrixClient#"crypto.roomKeyRequest" - * @param {module:crypto~IncomingRoomKeyRequest} req request details - */ -/** - * Fires when we receive a room key request cancellation - * - * @event module:client~MatrixClient#"crypto.roomKeyRequestCancellation" - * @param {module:crypto~IncomingRoomKeyRequestCancellation} req - */ - -/** - * Fires when the app may wish to warn the user about something related - * the end-to-end crypto. - * - * @event module:client~MatrixClient#"crypto.warning" - * @param {string} type One of the strings listed above - */ \ No newline at end of file +// a number of types are re-exported for backwards compatibility, in case any applications are referencing it. \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/key_passphrase.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/key_passphrase.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/key_passphrase.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/key_passphrase.js 2023-04-11 06:11:52.000000000 +0000 @@ -6,11 +6,8 @@ exports.deriveKey = deriveKey; exports.keyFromAuthData = keyFromAuthData; exports.keyFromPassphrase = keyFromPassphrase; - var _randomstring = require("../randomstring"); - -var _utils = require("../utils"); - +var _crypto = require("./crypto"); /* Copyright 2018 - 2021 The Matrix.org Foundation C.I.C. @@ -26,28 +23,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -const subtleCrypto = typeof window !== "undefined" && window.crypto ? window.crypto.subtle || window.crypto.webkitSubtle : null; + const DEFAULT_ITERATIONS = 500000; const DEFAULT_BITSIZE = 256; + /* eslint-disable camelcase */ function keyFromAuthData(authData, password) { if (!global.Olm) { throw new Error("Olm is not available"); } - if (!authData.private_key_salt || !authData.private_key_iterations) { throw new Error("Salt and/or iterations not found: " + "this backup cannot be restored with a passphrase"); } - return deriveKey(password, authData.private_key_salt, authData.private_key_iterations, authData.private_key_bits || DEFAULT_BITSIZE); } - async function keyFromPassphrase(password) { if (!global.Olm) { throw new Error("Olm is not available"); } - const salt = (0, _randomstring.randomString)(32); const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE); return { @@ -56,37 +50,18 @@ iterations: DEFAULT_ITERATIONS }; } - async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) { - return subtleCrypto ? deriveKeyBrowser(password, salt, iterations, numBits) : deriveKeyNode(password, salt, iterations, numBits); -} - -async function deriveKeyBrowser(password, salt, iterations, numBits) { - const subtleCrypto = global.crypto.subtle; - const TextEncoder = global.TextEncoder; - - if (!subtleCrypto || !TextEncoder) { - throw new Error("Password-based backup is not avaiable on this platform"); + if (!_crypto.subtleCrypto || !_crypto.TextEncoder) { + throw new Error("Password-based backup is not available on this platform"); } - - const key = await subtleCrypto.importKey('raw', new TextEncoder().encode(password), { - name: 'PBKDF2' - }, false, ['deriveBits']); - const keybits = await subtleCrypto.deriveBits({ - name: 'PBKDF2', - salt: new TextEncoder().encode(salt), + const key = await _crypto.subtleCrypto.importKey("raw", new _crypto.TextEncoder().encode(password), { + name: "PBKDF2" + }, false, ["deriveBits"]); + const keybits = await _crypto.subtleCrypto.deriveBits({ + name: "PBKDF2", + salt: new _crypto.TextEncoder().encode(salt), iterations: iterations, - hash: 'SHA-512' + hash: "SHA-512" }, key, numBits); return new Uint8Array(keybits); -} - -async function deriveKeyNode(password, salt, iterations, numBits) { - const crypto = (0, _utils.getCrypto)(); - - if (!crypto) { - throw new Error("No usable crypto implementation"); - } - - return crypto.pbkdf2Sync(password, Buffer.from(salt, 'binary'), iterations, numBits, 'sha512'); } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OlmDevice.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OlmDevice.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OlmDevice.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OlmDevice.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,29 +3,32 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.WITHHELD_MESSAGES = exports.OlmDevice = void 0; - +exports.WITHHELD_MESSAGES = exports.PayloadTooLargeError = exports.OlmDevice = void 0; var _logger = require("../logger"); - var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store"); - var algorithms = _interopRequireWildcard(require("./algorithms")); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // The maximum size of an event is 65K, and we base64 the content, so this is a // reasonable approximation to the biggest plaintext we can encrypt. const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; - +class PayloadTooLargeError extends Error { + constructor(...args) { + super(...args); + _defineProperty(this, "data", { + errcode: "M_TOO_LARGE", + error: "Payload too large for encrypted message" + }); + } +} +exports.PayloadTooLargeError = PayloadTooLargeError; function checkPayloadLength(payloadString) { if (payloadString === undefined) { throw new Error("payloadString undefined"); } - if (payloadString.length > MAX_PLAINTEXT_LENGTH) { // might as well fail early here rather than letting the olm library throw // a cryptic memory allocation error. @@ -33,50 +36,25 @@ // Note that even if we manage to do the encryption, the message send may fail, // because by the time we've wrapped the ciphertext in the event object, it may // exceed 65K. But at least we won't just fail with "abort()" in that case. - const err = new Error("Message too long (" + payloadString.length + " bytes). " + "The maximum for an encrypted message is " + MAX_PLAINTEXT_LENGTH + " bytes."); // TODO: [TypeScript] We should have our own error types - - err["data"] = { - errcode: "M_TOO_LARGE", - error: "Payload too large for encrypted message" - }; - throw err; + throw new PayloadTooLargeError(`Message too long (${payloadString.length} bytes). ` + `The maximum for an encrypted message is ${MAX_PLAINTEXT_LENGTH} bytes.`); } } /** - * The type of object we use for importing and exporting megolm session data. - * - * @typedef {Object} module:crypto/OlmDevice.MegolmSessionData - * @property {String} sender_key Sender's Curve25519 device key - * @property {String[]} forwarding_curve25519_key_chain Devices which forwarded - * this session to us (normally empty). - * @property {Object} sender_claimed_keys Other keys the sender claims. - * @property {String} room_id Room this session is used in - * @property {String} session_id Unique id for the session - * @property {String} session_key Base64'ed key data - */ - - -/* eslint-enable camelcase */ - -/** * Manages the olm cryptography functions. Each OlmDevice has a single * OlmAccount and a number of OlmSessions. * * Accounts and sessions are kept pickled in the cryptoStore. - * - * @constructor - * @alias module:crypto/OlmDevice - * - * @param {Object} cryptoStore A store for crypto data - * - * @property {string} deviceCurve25519Key Curve25519 key for the account - * @property {string} deviceEd25519Key Ed25519 key for the account */ class OlmDevice { // set by consumers - // don't know these until we load the account from storage in init() + + /** Curve25519 key for the account, unknown until we load the account from storage in init() */ + + /** Ed25519 key for the account, unknown until we load the account from storage in init() */ + // we don't bother stashing outboundgroupsessions in the cryptoStore - // instead we keep them here. + // Store a set of decrypted message indexes for each group session. // This partially mitigates a replay attack where a MITM resends a group // message into the room. @@ -93,38 +71,33 @@ // // Keys are strings of form "||" // Values are objects of the form "{id: , timestamp: }" + // Keep track of sessions that we're starting, so that we don't start // multiple sessions for the same device at the same time. // set by consumers + // Used by olm to serialise prekey message decryptions // set by consumers + constructor(cryptoStore) { this.cryptoStore = cryptoStore; - _defineProperty(this, "pickleKey", "DEFAULT_KEY"); - _defineProperty(this, "deviceCurve25519Key", null); - _defineProperty(this, "deviceEd25519Key", null); - _defineProperty(this, "maxOneTimeKeys", null); - _defineProperty(this, "outboundGroupSessionStore", {}); - _defineProperty(this, "inboundGroupSessionMessageIndexes", {}); - _defineProperty(this, "sessionsInProgress", {}); - _defineProperty(this, "olmPrekeyPromise", Promise.resolve()); } + /** - * @return {array} The version of Olm. + * @returns The version of Olm. */ - - static getOlmVersion() { return global.Olm.get_library_version(); } + /** * Initialise the OlmAccount. This must be called before any other operations * on the OlmDevice. @@ -137,60 +110,51 @@ * * Reads the device keys from the OlmAccount object. * - * @param {object} opts - * @param {object} opts.fromExportedDevice (Optional) data from exported device + * @param fromExportedDevice - (Optional) data from exported device * that must be re-created. * If present, opts.pickleKey is ignored * (exported data already provides a pickle key) - * @param {object} opts.pickleKey (Optional) pickle key to set instead of default one + * @param pickleKey - (Optional) pickle key to set instead of default one */ - - async init({ pickleKey, fromExportedDevice } = {}) { let e2eKeys; const account = new global.Olm.Account(); - try { if (fromExportedDevice) { if (pickleKey) { - _logger.logger.warn('ignoring opts.pickleKey' + ' because opts.fromExportedDevice is present.'); + _logger.logger.warn("ignoring opts.pickleKey" + " because opts.fromExportedDevice is present."); } - this.pickleKey = fromExportedDevice.pickleKey; await this.initialiseFromExportedDevice(fromExportedDevice, account); } else { if (pickleKey) { this.pickleKey = pickleKey; } - await this.initialiseAccount(account); } - e2eKeys = JSON.parse(account.identity_keys()); this.maxOneTimeKeys = account.max_number_of_one_time_keys(); } finally { account.free(); } - this.deviceCurve25519Key = e2eKeys.curve25519; this.deviceEd25519Key = e2eKeys.ed25519; } + /** * Populates the crypto store using data that was exported from an existing device. * Note that for now only the “account” and “sessions” stores are populated; * Other stores will be as with a new device. * - * @param {IExportedDevice} exportedData Data exported from another device + * @param exportedData - Data exported from another device * through the “export” method. - * @param {Olm.Account} account an olm account to initialize + * @param account - an olm account to initialize */ - - async initialiseFromExportedDevice(exportedData, account) { - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { this.cryptoStore.storeAccount(txn, exportedData.pickledAccount); exportedData.sessions.forEach(session => { const { @@ -206,9 +170,8 @@ }); account.unpickle(this.pickleKey, exportedData.pickledAccount); } - async initialiseAccount(account) { - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.cryptoStore.getAccount(txn, pickledAccount => { if (pickledAccount !== null) { account.unpickle(this.pickleKey, pickledAccount); @@ -220,6 +183,7 @@ }); }); } + /** * extract our OlmAccount from the crypto store and call the given function * with the account object @@ -229,16 +193,12 @@ * This function requires a live transaction object from cryptoStore.doTxn() * and therefore may only be called in a doTxn() callback. * - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {function} func - * @private + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @internal */ - - getAccount(txn, func) { this.cryptoStore.getAccount(txn, pickledAccount => { const account = new global.Olm.Account(); - try { account.unpickle(this.pickleKey, pickledAccount); func(account); @@ -247,79 +207,69 @@ } }); } + /* * Saves an account to the crypto store. * This function requires a live transaction object from cryptoStore.doTxn() * and therefore may only be called in a doTxn() callback. * - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {object} Olm.Account object - * @private + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @param Olm.Account object + * @internal */ - - storeAccount(txn, account) { this.cryptoStore.storeAccount(txn, account.pickle(this.pickleKey)); } + /** * Export data for re-creating the Olm device later. * TODO export data other than just account and (P2P) sessions. * - * @return {Promise} The exported data + * @returns The exported data */ - - async export() { const result = { pickleKey: this.pickleKey }; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { this.cryptoStore.getAccount(txn, pickledAccount => { result.pickledAccount = pickledAccount; }); - result.sessions = []; // Note that the pickledSession object we get in the callback + result.sessions = []; + // Note that the pickledSession object we get in the callback // is not exactly the same thing you get in method _getSession // see documentation of IndexedDBCryptoStore.getAllEndToEndSessions - this.cryptoStore.getAllEndToEndSessions(txn, pickledSession => { result.sessions.push(pickledSession); }); }); return result; } + /** * extract an OlmSession from the session store and call the given function * The session is usable only within the callback passed to this * function and will be freed as soon the callback returns. It is *not* * usable for the rest of the lifetime of the transaction. * - * @param {string} deviceKey - * @param {string} sessionId - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {function} func - * @private + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @internal */ - - getSession(deviceKey, sessionId, txn, func) { this.cryptoStore.getEndToEndSession(deviceKey, sessionId, txn, sessionInfo => { this.unpickleSession(sessionInfo, func); }); } + /** * Creates a session object from a session pickle and executes the given * function with it. The session object is destroyed once the function * returns. * - * @param {object} sessionInfo - * @param {function} func - * @private + * @internal */ - - unpickleSession(sessionInfo, func) { const session = new global.Olm.Session(); - try { session.unpickle(this.pickleKey, sessionInfo.session); const unpickledSessInfo = Object.assign({}, sessionInfo, { @@ -330,166 +280,156 @@ session.free(); } } + /** * store our OlmSession in the session store * - * @param {string} deviceKey - * @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int} - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @private + * @param sessionInfo - `{session: OlmSession, lastReceivedMessageTs: int}` + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @internal */ - - saveSession(deviceKey, sessionInfo, txn) { const sessionId = sessionInfo.session.session_id(); + _logger.logger.debug(`Saving Olm session ${sessionId} with device ${deviceKey}: ${sessionInfo.session.describe()}`); + + // Why do we re-use the input object for this, overwriting the same key with a different + // type? Is it because we want to erase the unpickled session to enforce that it's no longer + // used? A comment would be great. const pickledSessionInfo = Object.assign(sessionInfo, { session: sessionInfo.session.pickle(this.pickleKey) }); this.cryptoStore.storeEndToEndSession(deviceKey, sessionId, pickledSessionInfo, txn); } + /** * get an OlmUtility and call the given function * - * @param {function} func - * @return {object} result of func - * @private + * @returns result of func + * @internal */ - - getUtility(func) { const utility = new global.Olm.Utility(); - try { return func(utility); } finally { utility.free(); } } + /** * Signs a message with the ed25519 key for this account. * - * @param {string} message message to be signed - * @return {Promise} base64-encoded signature + * @param message - message to be signed + * @returns base64-encoded signature */ - - async sign(message) { let result; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.getAccount(txn, account => { result = account.sign(message); }); }); return result; } + /** * Get the current (unused, unpublished) one-time keys for this account. * - * @return {object} one time keys; an object with the single property + * @returns one time keys; an object with the single property * curve25519, which is itself an object mapping key id to Curve25519 * key. */ - - async getOneTimeKeys() { let result; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.getAccount(txn, account => { result = JSON.parse(account.one_time_keys()); }); }); return result; } + /** * Get the maximum number of one-time keys we can store. * - * @return {number} number of keys + * @returns number of keys */ - - maxNumberOfOneTimeKeys() { - return this.maxOneTimeKeys; + return this.maxOneTimeKeys ?? -1; } + /** * Marks all of the one-time keys as published. */ - - async markKeysAsPublished() { - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.getAccount(txn, account => { account.mark_keys_as_published(); this.storeAccount(txn, account); }); }); } + /** * Generate some new one-time keys * - * @param {number} numKeys number of keys to generate - * @return {Promise} Resolved once the account is saved back having generated the keys + * @param numKeys - number of keys to generate + * @returns Resolved once the account is saved back having generated the keys */ - - generateOneTimeKeys(numKeys) { - return this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + return this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.getAccount(txn, account => { account.generate_one_time_keys(numKeys); this.storeAccount(txn, account); }); }); } + /** * Generate a new fallback keys * - * @return {Promise} Resolved once the account is saved back having generated the key + * @returns Resolved once the account is saved back having generated the key */ - - async generateFallbackKey() { - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.getAccount(txn, account => { account.generate_fallback_key(); this.storeAccount(txn, account); }); }); } - async getFallbackKey() { let result; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.getAccount(txn, account => { result = JSON.parse(account.unpublished_fallback_key()); }); }); return result; } - async forgetOldFallbackKey() { - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT], txn => { this.getAccount(txn, account => { account.forget_old_fallback_key(); this.storeAccount(txn, account); }); }); } + /** * Generate a new outbound session * * The new session will be stored in the cryptoStore. * - * @param {string} theirIdentityKey remote user's Curve25519 identity key - * @param {string} theirOneTimeKey remote user's one-time Curve25519 key - * @return {string} sessionId for the outbound session. + * @param theirIdentityKey - remote user's Curve25519 identity key + * @param theirOneTimeKey - remote user's one-time Curve25519 key + * @returns sessionId for the outbound session. */ - - async createOutboundSession(theirIdentityKey, theirOneTimeKey) { let newSessionId; - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { this.getAccount(txn, account => { const session = new global.Olm.Session(); - try { session.create_outbound(account, theirIdentityKey, theirOneTimeKey); newSessionId = session.session_id(); @@ -509,32 +449,27 @@ }, _logger.logger.withPrefix("[createOutboundSession]")); return newSessionId; } + /** * Generate a new inbound session, given an incoming message * - * @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key - * @param {number} messageType messageType field from the received message (must be 0) - * @param {string} ciphertext base64-encoded body from the received message + * @param theirDeviceIdentityKey - remote user's Curve25519 identity key + * @param messageType - messageType field from the received message (must be 0) + * @param ciphertext - base64-encoded body from the received message * - * @return {{payload: string, session_id: string}} decrypted payload, and + * @returns decrypted payload, and * session id of new session * - * @raises {Error} if the received message was not valid (for instance, it - * didn't use a valid one-time key). + * @throws Error if the received message was not valid (for instance, it didn't use a valid one-time key). */ - - async createInboundSession(theirDeviceIdentityKey, messageType, ciphertext) { if (messageType !== 0) { throw new Error("Need messageType == 0 to create inbound session"); } - let result; // eslint-disable-line camelcase - - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ACCOUNT, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { this.getAccount(txn, account => { const session = new global.Olm.Session(); - try { session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext); account.remove_one_time_keys(session); @@ -558,72 +493,64 @@ }, _logger.logger.withPrefix("[createInboundSession]")); return result; } + /** * Get a list of known session IDs for the given device * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @return {Promise} a list of known session ids for the device + * @returns a list of known session ids for the device */ - - async getSessionIdsForDevice(theirDeviceIdentityKey) { const log = _logger.logger.withPrefix("[getSessionIdsForDevice]"); - - if (this.sessionsInProgress[theirDeviceIdentityKey]) { + if (theirDeviceIdentityKey in this.sessionsInProgress) { log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`); - try { await this.sessionsInProgress[theirDeviceIdentityKey]; - } catch (e) {// if the session failed to be created, just fall through and + } catch (e) { + // if the session failed to be created, just fall through and // return an empty result } } - let sessionIds; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { this.cryptoStore.getEndToEndSessions(theirDeviceIdentityKey, txn, sessions => { sessionIds = Object.keys(sessions); }); }, log); return sessionIds; } + /** * Get the right olm session id for encrypting messages to the given identity key * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {boolean} nowait Don't wait for an in-progress session to complete. + * @param nowait - Don't wait for an in-progress session to complete. * This should only be set to true of the calling function is the function * that marked the session as being in-progress. - * @param {Logger} [log] A possibly customised log - * @return {Promise} session id, or null if no established session + * @param log - A possibly customised log + * @returns session id, or null if no established session */ - - async getSessionIdForDevice(theirDeviceIdentityKey, nowait = false, log) { const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait, log); - if (sessionInfos.length === 0) { return null; - } // Use the session that has most recently received a message - - + } + // Use the session that has most recently received a message let idxOfBest = 0; - for (let i = 1; i < sessionInfos.length; i++) { const thisSessInfo = sessionInfos[i]; const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ? 0 : thisSessInfo.lastReceivedMessageTs; const bestSessInfo = sessionInfos[idxOfBest]; const bestLastReceived = bestSessInfo.lastReceivedMessageTs === undefined ? 0 : bestSessInfo.lastReceivedMessageTs; - if (thisLastReceived > bestLastReceived || thisLastReceived === bestLastReceived && thisSessInfo.sessionId < bestSessInfo.sessionId) { idxOfBest = i; } } - return sessionInfos[idxOfBest].sessionId; } + /** * Get information on the active Olm sessions for a device. *

@@ -632,39 +559,33 @@ * the keys 'hasReceivedMessage' (true if the session has received an incoming * message and is therefore past the pre-key stage), and 'sessionId'. * - * @param {string} deviceIdentityKey Curve25519 identity key for the device - * @param {boolean} nowait Don't wait for an in-progress session to complete. + * @param deviceIdentityKey - Curve25519 identity key for the device + * @param nowait - Don't wait for an in-progress session to complete. * This should only be set to true of the calling function is the function * that marked the session as being in-progress. - * @param {Logger} [log] A possibly customised log - * @return {Array.<{sessionId: string, hasReceivedMessage: boolean}>} + * @param log - A possibly customised log */ - - async getSessionInfoForDevice(deviceIdentityKey, nowait = false, log = _logger.logger) { log = log.withPrefix("[getSessionInfoForDevice]"); - - if (this.sessionsInProgress[deviceIdentityKey] && !nowait) { + if (deviceIdentityKey in this.sessionsInProgress && !nowait) { log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`); - try { await this.sessionsInProgress[deviceIdentityKey]; - } catch (e) {// if the session failed to be created, then just fall through and + } catch (e) { + // if the session failed to be created, then just fall through and // return an empty result } } - const info = []; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { this.cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, sessions => { const sessionIds = Object.keys(sessions).sort(); - for (const sessionId of sessionIds) { this.unpickleSession(sessions[sessionId], sessInfo => { info.push({ lastReceivedMessageTs: sessInfo.lastReceivedMessageTs, hasReceivedMessage: sessInfo.session.has_received_message(), - sessionId: sessionId + sessionId }); }); } @@ -672,54 +593,48 @@ }, log); return info; } + /** * Encrypt an outgoing message using an existing session * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {string} sessionId the id of the active session - * @param {string} payloadString payload to be encrypted and sent + * @param sessionId - the id of the active session + * @param payloadString - payload to be encrypted and sent * - * @return {Promise} ciphertext + * @returns ciphertext */ - - async encryptMessage(theirDeviceIdentityKey, sessionId, payloadString) { checkPayloadLength(payloadString); let res; - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { this.getSession(theirDeviceIdentityKey, sessionId, txn, sessionInfo => { const sessionDesc = sessionInfo.session.describe(); - _logger.logger.log("encryptMessage: Olm Session ID " + sessionId + " to " + theirDeviceIdentityKey + ": " + sessionDesc); - res = sessionInfo.session.encrypt(payloadString); this.saveSession(theirDeviceIdentityKey, sessionInfo, txn); }); }, _logger.logger.withPrefix("[encryptMessage]")); return res; } + /** * Decrypt an incoming message using an existing session * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {string} sessionId the id of the active session - * @param {number} messageType messageType field from the received message - * @param {string} ciphertext base64-encoded body from the received message + * @param sessionId - the id of the active session + * @param messageType - messageType field from the received message + * @param ciphertext - base64-encoded body from the received message * - * @return {Promise} decrypted payload. + * @returns decrypted payload. */ - - async decryptMessage(theirDeviceIdentityKey, sessionId, messageType, ciphertext) { let payloadString; - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { this.getSession(theirDeviceIdentityKey, sessionId, txn, sessionInfo => { const sessionDesc = sessionInfo.session.describe(); - _logger.logger.log("decryptMessage: Olm Session ID " + sessionId + " from " + theirDeviceIdentityKey + ": " + sessionDesc); - payloadString = sessionInfo.session.decrypt(messageType, ciphertext); sessionInfo.lastReceivedMessageTs = Date.now(); this.saveSession(theirDeviceIdentityKey, sessionInfo, txn); @@ -727,78 +642,67 @@ }, _logger.logger.withPrefix("[decryptMessage]")); return payloadString; } + /** * Determine if an incoming messages is a prekey message matching an existing session * - * @param {string} theirDeviceIdentityKey Curve25519 identity key for the + * @param theirDeviceIdentityKey - Curve25519 identity key for the * remote device - * @param {string} sessionId the id of the active session - * @param {number} messageType messageType field from the received message - * @param {string} ciphertext base64-encoded body from the received message + * @param sessionId - the id of the active session + * @param messageType - messageType field from the received message + * @param ciphertext - base64-encoded body from the received message * - * @return {Promise} true if the received message is a prekey message which matches + * @returns true if the received message is a prekey message which matches * the given session. */ - - async matchesSession(theirDeviceIdentityKey, sessionId, messageType, ciphertext) { if (messageType !== 0) { return false; } - let matches; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SESSIONS], txn => { this.getSession(theirDeviceIdentityKey, sessionId, txn, sessionInfo => { matches = sessionInfo.session.matches_inbound(ciphertext); }); }, _logger.logger.withPrefix("[matchesSession]")); return matches; } - async recordSessionProblem(deviceKey, type, fixed) { + _logger.logger.info(`Recording problem on olm session with ${deviceKey} of type ${type}. Recreating: ${fixed}`); await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed); } - sessionMayHaveProblems(deviceKey, timestamp) { return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp); } - filterOutNotifiedErrorDevices(devices) { return this.cryptoStore.filterOutNotifiedErrorDevices(devices); - } // Outbound group session + } + + // Outbound group session // ====================== /** * store an OutboundGroupSession in outboundGroupSessionStore * - * @param {Olm.OutboundGroupSession} session - * @private + * @internal */ - - saveOutboundGroupSession(session) { this.outboundGroupSessionStore[session.session_id()] = session.pickle(this.pickleKey); } + /** * extract an OutboundGroupSession from outboundGroupSessionStore and call the * given function * - * @param {string} sessionId - * @param {function} func - * @return {object} result of func - * @private + * @returns result of func + * @internal */ - - getOutboundGroupSession(sessionId, func) { const pickled = this.outboundGroupSessionStore[sessionId]; - if (pickled === undefined) { throw new Error("Unknown outbound group session " + sessionId); } - const session = new global.Olm.OutboundGroupSession(); - try { session.unpickle(this.pickleKey, pickled); return func(session); @@ -806,16 +710,14 @@ session.free(); } } + /** * Generate a new outbound group session * - * @return {string} sessionId for the outbound session. + * @returns sessionId for the outbound session. */ - - createOutboundGroupSession() { const session = new global.Olm.OutboundGroupSession(); - try { session.create(); this.saveOutboundGroupSession(session); @@ -824,19 +726,17 @@ session.free(); } } + /** * Encrypt an outgoing message with an outbound group session * - * @param {string} sessionId the id of the outboundgroupsession - * @param {string} payloadString payload to be encrypted and sent + * @param sessionId - the id of the outboundgroupsession + * @param payloadString - payload to be encrypted and sent * - * @return {string} ciphertext + * @returns ciphertext */ - - encryptGroupMessage(sessionId, payloadString) { _logger.logger.log(`encrypting msg with megolm session ${sessionId}`); - checkPayloadLength(payloadString); return this.getOutboundGroupSession(sessionId, session => { const res = session.encrypt(payloadString); @@ -844,16 +744,15 @@ return res; }); } + /** * Get the session keys for an outbound group session * - * @param {string} sessionId the id of the outbound group session + * @param sessionId - the id of the outbound group session * - * @return {{chain_index: number, key: string}} current chain index, and + * @returns current chain index, and * base64-encoded secret key. */ - - getOutboundGroupSessionKey(sessionId) { return this.getOutboundGroupSession(sessionId, function (session) { return { @@ -861,22 +760,21 @@ key: session.session_key() }; }); - } // Inbound group session + } + + // Inbound group session // ===================== /** * Unpickle a session from a sessionData object and invoke the given function. * The session is valid only until func returns. * - * @param {Object} sessionData Object describing the session. - * @param {function(Olm.InboundGroupSession)} func Invoked with the unpickled session - * @return {*} result of func + * @param sessionData - Object describing the session. + * @param func - Invoked with the unpickled session + * @returns result of func */ - - unpickleInboundGroupSession(sessionData, func) { const session = new global.Olm.InboundGroupSession(); - try { session.unpickle(this.pickleKey, sessionData.session); return func(session); @@ -884,87 +782,75 @@ session.free(); } } + /** * extract an InboundGroupSession from the crypto store and call the given function * - * @param {string} roomId The room ID to extract the session for, or null to fetch + * @param roomId - The room ID to extract the session for, or null to fetch * sessions for any room. - * @param {string} senderKey - * @param {string} sessionId - * @param {*} txn Opaque transaction object from cryptoStore.doTxn() - * @param {function(Olm.InboundGroupSession, InboundGroupSessionData)} func - * function to call. + * @param txn - Opaque transaction object from cryptoStore.doTxn() + * @param func - function to call. * - * @private + * @internal */ - - getInboundGroupSession(roomId, senderKey, sessionId, txn, func) { this.cryptoStore.getEndToEndInboundGroupSession(senderKey, sessionId, txn, (sessionData, withheld) => { if (sessionData === null) { func(null, null, withheld); return; - } // if we were given a room ID, check that the it matches the original one for the session. This stops - // the HS pretending a message was targeting a different room. - + } + // if we were given a room ID, check that the it matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. if (roomId !== null && roomId !== sessionData.room_id) { throw new Error("Mismatched room_id for inbound group session (expected " + sessionData.room_id + ", was " + roomId + ")"); } - this.unpickleInboundGroupSession(sessionData, session => { func(session, sessionData, withheld); }); }); } + /** * Add an inbound group session to the session store * - * @param {string} roomId room in which this session will be used - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {Array} forwardingCurve25519KeyChain Devices involved in forwarding + * @param roomId - room in which this session will be used + * @param senderKey - base64-encoded curve25519 key of the sender + * @param forwardingCurve25519KeyChain - Devices involved in forwarding * this session to us. - * @param {string} sessionId session identifier - * @param {string} sessionKey base64-encoded secret key - * @param {Object} keysClaimed Other keys the sender claims. - * @param {boolean} exportFormat true if the megolm keys are in export format + * @param sessionId - session identifier + * @param sessionKey - base64-encoded secret key + * @param keysClaimed - Other keys the sender claims. + * @param exportFormat - true if the megolm keys are in export format * (ie, they lack an ed25519 signature) - * @param {Object} [extraSessionData={}] any other data to be include with the session + * @param extraSessionData - any other data to be include with the session */ - - async addInboundGroupSession(roomId, senderKey, forwardingCurve25519KeyChain, sessionId, sessionKey, keysClaimed, exportFormat, extraSessionData = {}) { - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS], txn => { /* if we already have this session, consider updating it */ this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (existingSession, existingSessionData) => { // new session. const session = new global.Olm.InboundGroupSession(); - try { if (exportFormat) { session.import_session(sessionKey); } else { session.create(sessionKey); } - if (sessionId != session.session_id()) { throw new Error("Mismatched group session ID from senderKey: " + senderKey); } - if (existingSession) { - _logger.logger.log("Update for megolm session " + senderKey + "/" + sessionId); - + _logger.logger.log(`Update for megolm session ${senderKey}|${sessionId}`); if (existingSession.first_known_index() <= session.first_known_index()) { if (!existingSessionData.untrusted || extraSessionData.untrusted) { // existing session has less-than-or-equal index // (i.e. can decrypt at least as much), and the // new session's trust does not win over the old // session's trust, so keep it - _logger.logger.log(`Keeping existing megolm session ${sessionId}`); - + _logger.logger.log(`Keeping existing megolm session ${senderKey}|${sessionId}`); return; } - if (existingSession.first_known_index() < session.first_known_index()) { // We want to upgrade the existing session's trust, // but we can't just use the new session because we'll @@ -972,22 +858,19 @@ // properly, and then manually set the existing session // as trusted. if (existingSession.export_session(session.first_known_index()) === session.export_session(session.first_known_index())) { - _logger.logger.info("Upgrading trust of existing megolm session " + sessionId + " based on newly-received trusted session"); - + _logger.logger.info("Upgrading trust of existing megolm session " + `${senderKey}|${sessionId} based on newly-received trusted session`); existingSessionData.untrusted = false; this.cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, existingSessionData, txn); } else { - _logger.logger.warn("Newly-received megolm session " + sessionId + " does not match existing session! Keeping existing session"); + _logger.logger.warn(`Newly-received megolm session ${senderKey}|$sessionId}` + " does not match existing session! Keeping existing session"); } - return; - } // If the sessions have the same index, go ahead and store the new trusted one. - + } + // If the sessions have the same index, go ahead and store the new trusted one. } } - _logger.logger.info("Storing megolm session " + senderKey + "/" + sessionId + " with first index " + session.first_known_index()); - + _logger.logger.info(`Storing megolm session ${senderKey}|${sessionId} with first index ` + session.first_known_index()); const sessionData = Object.assign({}, extraSessionData, { room_id: roomId, session: session.pickle(this.pickleKey), @@ -995,7 +878,6 @@ forwardingCurve25519KeyChain: forwardingCurve25519KeyChain }); this.cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn); - if (!existingSession && extraSessionData.sharedHistory) { this.cryptoStore.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn); } @@ -1005,19 +887,18 @@ }); }, _logger.logger.withPrefix("[addInboundGroupSession]")); } + /** * Record in the data store why an inbound group session was withheld. * - * @param {string} roomId room that the session belongs to - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {string} code reason code - * @param {string} reason human-readable version of `code` + * @param roomId - room that the session belongs to + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param code - reason code + * @param reason - human-readable version of `code` */ - - async addInboundGroupSessionWithheld(roomId, senderKey, sessionId, code, reason) { - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { this.cryptoStore.storeEndToEndInboundGroupSessionWithheld(senderKey, sessionId, { room_id: roomId, code: code, @@ -1025,64 +906,53 @@ }, txn); }); } + /** * Decrypt a received message with an inbound group session * - * @param {string} roomId room in which the message was received - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {string} body base64-encoded body of the encrypted message - * @param {string} eventId ID of the event being decrypted - * @param {Number} timestamp timestamp of the event being decrypted - * - * @return {null} the sessionId is unknown - * - * @return {Promise<{result: string, senderKey: string, - * forwardingCurve25519KeyChain: Array, - * keysClaimed: Object}>} + * @param roomId - room in which the message was received + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param body - base64-encoded body of the encrypted message + * @param eventId - ID of the event being decrypted + * @param timestamp - timestamp of the event being decrypted + * + * @returns null if the sessionId is unknown */ - - async decryptGroupMessage(roomId, senderKey, sessionId, body, eventId, timestamp) { - let result; // when the localstorage crypto store is used as an indexeddb backend, + let result = null; + // when the localstorage crypto store is used as an indexeddb backend, // exceptions thrown from within the inner function are not passed through // to the top level, so we store exceptions in a variable and raise them at // the end - let error; - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => { - if (session === null) { + if (session === null || sessionData === null) { if (withheld) { error = new algorithms.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", calculateWithheldMessage(withheld), { - session: senderKey + '|' + sessionId + session: senderKey + "|" + sessionId }); } - result = null; return; } - let res; - try { res = session.decrypt(body); } catch (e) { - if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) { + if (e?.message === "OLM.UNKNOWN_MESSAGE_INDEX" && withheld) { error = new algorithms.DecryptionError("MEGOLM_UNKNOWN_INBOUND_SESSION_ID", calculateWithheldMessage(withheld), { - session: senderKey + '|' + sessionId + session: senderKey + "|" + sessionId }); } else { error = e; } - return; } - let plaintext = res.plaintext; - if (plaintext === undefined) { - // Compatibility for older olm versions. + // @ts-ignore - Compatibility for older olm versions. plaintext = res; } else { // Check if we have seen this message index before to detect replay attacks. @@ -1090,22 +960,18 @@ // and timestamp from the last time we used this message index, then we // don't consider it a replay attack. const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index; - if (messageIndexKey in this.inboundGroupSessionMessageIndexes) { const msgInfo = this.inboundGroupSessionMessageIndexes[messageIndexKey]; - if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) { error = new Error("Duplicate message index, possible replay attack: " + messageIndexKey); return; } } - this.inboundGroupSessionMessageIndexes[messageIndexKey] = { id: eventId, timestamp: timestamp }; } - sessionData.session = session.pickle(this.pickleKey); this.cryptoStore.storeEndToEndInboundGroupSession(senderKey, sessionId, sessionData, txn); result = { @@ -1113,40 +979,35 @@ keysClaimed: sessionData.keysClaimed || {}, senderKey: senderKey, forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [], - untrusted: sessionData.untrusted + untrusted: !!sessionData.untrusted }; }); }, _logger.logger.withPrefix("[decryptGroupMessage]")); - if (error) { throw error; } - return result; } + /** * Determine if we have the keys for a given megolm session * - * @param {string} roomId room in which the message was received - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier + * @param roomId - room in which the message was received + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier * - * @returns {Promise} true if we have the keys to this session + * @returns true if we have the keys to this session */ - - async hasInboundSessionKeys(roomId, senderKey, sessionId) { let result; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { this.cryptoStore.getEndToEndInboundGroupSession(senderKey, sessionId, txn, sessionData => { if (sessionData === null) { result = false; return; } - if (roomId !== sessionData.room_id) { _logger.logger.warn(`requested keys for inbound group session ${senderKey}|` + `${sessionId}, with incorrect room_id ` + `(expected ${sessionData.room_id}, ` + `was ${roomId})`); - result = false; } else { result = true; @@ -1155,72 +1016,65 @@ }, _logger.logger.withPrefix("[hasInboundSessionKeys]")); return result; } + /** * Extract the keys to a given megolm session, for sharing * - * @param {string} roomId room in which the message was received - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {number} chainIndex The chain index at which to export the session. + * @param roomId - room in which the message was received + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param chainIndex - The chain index at which to export the session. * If omitted, export at the first index we know about. * - * @returns {Promise<{chain_index: number, key: string, - * forwarding_curve25519_key_chain: Array, - * sender_claimed_ed25519_key: string - * }>} + * @returns * details of the session key. The key is a base64-encoded megolm key in * export format. * * @throws Error If the given chain index could not be obtained from the known * index (ie. the given chain index is before the first we have). */ - - async getInboundGroupSessionKey(roomId, senderKey, sessionId, chainIndex) { - let result; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + let result = null; + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, _indexeddbCryptoStore.IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { this.getInboundGroupSession(roomId, senderKey, sessionId, txn, (session, sessionData) => { - if (session === null) { + if (session === null || sessionData === null) { result = null; return; } - if (chainIndex === undefined) { chainIndex = session.first_known_index(); } - const exportedSession = session.export_session(chainIndex); const claimedKeys = sessionData.keysClaimed || {}; const senderEd25519Key = claimedKeys.ed25519 || null; - const forwardingKeyChain = sessionData.forwardingCurve25519KeyChain || []; // older forwarded keys didn't set the "untrusted" + const forwardingKeyChain = sessionData.forwardingCurve25519KeyChain || []; + // older forwarded keys didn't set the "untrusted" // property, but can be identified by having a // non-empty forwarding key chain. These keys should // be marked as untrusted since we don't know that they // can be trusted - const untrusted = "untrusted" in sessionData ? sessionData.untrusted : forwardingKeyChain.length > 0; result = { - "chain_index": chainIndex, - "key": exportedSession, - "forwarding_curve25519_key_chain": forwardingKeyChain, - "sender_claimed_ed25519_key": senderEd25519Key, - "shared_history": sessionData.sharedHistory || false, - "untrusted": untrusted + chain_index: chainIndex, + key: exportedSession, + forwarding_curve25519_key_chain: forwardingKeyChain, + sender_claimed_ed25519_key: senderEd25519Key, + shared_history: sessionData.sharedHistory || false, + untrusted: untrusted }; }); }, _logger.logger.withPrefix("[getInboundGroupSessionKey]")); return result; } + /** * Export an inbound group session * - * @param {string} senderKey base64-encoded curve25519 key of the sender - * @param {string} sessionId session identifier - * @param {ISessionInfo} sessionData The session object from the store - * @return {module:crypto/OlmDevice.MegolmSessionData} exported session data + * @param senderKey - base64-encoded curve25519 key of the sender + * @param sessionId - session identifier + * @param sessionData - The session object from the store + * @returns exported session data */ - - exportInboundGroupSession(senderKey, sessionId, sessionData) { return this.unpickleInboundGroupSession(sessionData, session => { const messageIndex = session.first_known_index(); @@ -1236,37 +1090,34 @@ }; }); } - async getSharedHistoryInboundGroupSessions(roomId) { let result; - await this.cryptoStore.doTxn('readonly', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS], txn => { + await this.cryptoStore.doTxn("readonly", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS], txn => { result = this.cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn); }, _logger.logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]")); return result; - } // Utilities + } + + // Utilities // ========= /** * Verify an ed25519 signature. * - * @param {string} key ed25519 key - * @param {string} message message which was signed - * @param {string} signature base64-encoded signature to be checked + * @param key - ed25519 key + * @param message - message which was signed + * @param signature - base64-encoded signature to be checked * - * @raises {Error} if there is a problem with the verification. If the key was + * @throws Error if there is a problem with the verification. If the key was * too small then the message will be "OLM.INVALID_BASE64". If the signature * was invalid then the message will be "OLM.BAD_MESSAGE_MAC". */ - - verifySignature(key, message, signature) { this.getUtility(function (util) { util.ed25519_verify(key, message, signature); }); } - } - exports.OlmDevice = OlmDevice; const WITHHELD_MESSAGES = { "m.unverified": "The sender has disabled encrypting to unverified devices.", @@ -1274,18 +1125,17 @@ "m.unauthorised": "You are not authorised to read the message.", "m.no_olm": "Unable to establish a secure channel." }; + /** * Calculate the message to use for the exception when a session key is withheld. * - * @param {object} withheld An object that describes why the key was withheld. + * @param withheld - An object that describes why the key was withheld. * - * @return {string} the message + * @returns the message * - * @private + * @internal */ - exports.WITHHELD_MESSAGES = WITHHELD_MESSAGES; - function calculateWithheldMessage(withheld) { if (withheld.code && withheld.code in WITHHELD_MESSAGES) { return WITHHELD_MESSAGES[withheld.code]; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/olmlib.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/olmlib.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/olmlib.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/olmlib.js 2023-04-11 06:11:52.000000000 +0000 @@ -14,74 +14,47 @@ exports.pkSign = pkSign; exports.pkVerify = pkVerify; exports.verifySignature = verifySignature; - var _anotherJson = _interopRequireDefault(require("another-json")); - var _logger = require("../logger"); - var _event = require("../@types/event"); - +var _utils = require("../utils"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/* -Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/** - * @module olmlib - * - * Utilities common to olm encryption algorithms - */ +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } var Algorithm; /** * matrix algorithm tag for olm */ - (function (Algorithm) { Algorithm["Olm"] = "m.olm.v1.curve25519-aes-sha2"; Algorithm["Megolm"] = "m.megolm.v1.aes-sha2"; Algorithm["MegolmBackup"] = "m.megolm_backup.v1.curve25519-aes-sha2"; })(Algorithm || (Algorithm = {})); - const OLM_ALGORITHM = Algorithm.Olm; + /** * matrix algorithm tag for megolm */ - exports.OLM_ALGORITHM = OLM_ALGORITHM; const MEGOLM_ALGORITHM = Algorithm.Megolm; + /** * matrix algorithm tag for megolm backups */ - exports.MEGOLM_ALGORITHM = MEGOLM_ALGORITHM; const MEGOLM_BACKUP_ALGORITHM = Algorithm.MegolmBackup; exports.MEGOLM_BACKUP_ALGORITHM = MEGOLM_BACKUP_ALGORITHM; - /** * Encrypt an event payload for an Olm device * - * @param {Object} resultsObject The `ciphertext` property + * @param resultsObject - The `ciphertext` property * of the m.room.encrypted event to which to add our result * - * @param {string} ourUserId - * @param {string} ourDeviceId - * @param {module:crypto/OlmDevice} olmDevice olm.js wrapper - * @param {string} recipientUserId - * @param {module:crypto/deviceinfo} recipientDevice - * @param {object} payloadFields fields to include in the encrypted payload + * @param olmDevice - olm.js wrapper + * @param payloadFields - fields to include in the encrypted payload * * Returns a promise which resolves (to undefined) when the payload * has been encrypted into `resultsObject` @@ -89,16 +62,14 @@ async function encryptMessageForDevice(resultsObject, ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice, payloadFields) { const deviceKey = recipientDevice.getIdentityKey(); const sessionId = await olmDevice.getSessionIdForDevice(deviceKey); - if (sessionId === null) { // If we don't have a session for a device then // we can't encrypt a message for it. + _logger.logger.log(`[olmlib.encryptMessageForDevice] Unable to find Olm session for device ` + `${recipientUserId}:${recipientDevice.deviceId}`); return; } - - _logger.logger.log("Using sessionid " + sessionId + " for device " + recipientUserId + ":" + recipientDevice.deviceId); - - const payload = { + _logger.logger.log(`[olmlib.encryptMessageForDevice] Using Olm session ${sessionId} for device ` + `${recipientUserId}:${recipientDevice.deviceId}`); + const payload = _objectSpread({ sender: ourUserId, // TODO this appears to no longer be used whatsoever sender_device: ourDeviceId, @@ -111,148 +82,125 @@ // the curve25519 key and the ed25519 key are owned by // the same device. keys: { - "ed25519": olmDevice.deviceEd25519Key + ed25519: olmDevice.deviceEd25519Key }, // include the recipient device details in the payload, // to avoid unknown key attacks, per // https://github.com/vector-im/vector-web/issues/2483 recipient: recipientUserId, recipient_keys: { - "ed25519": recipientDevice.getFingerprint() + ed25519: recipientDevice.getFingerprint() } - }; // TODO: technically, a bunch of that stuff only needs to be included for + }, payloadFields); + + // TODO: technically, a bunch of that stuff only needs to be included for // pre-key messages: after that, both sides know exactly which devices are // involved in the session. If we're looking to reduce data transfer in the // future, we could elide them for subsequent messages. - Object.assign(payload, payloadFields); resultsObject[deviceKey] = await olmDevice.encryptMessage(deviceKey, sessionId, JSON.stringify(payload)); } - /** * Get the existing olm sessions for the given devices, and the devices that * don't have olm sessions. * - * @param {module:crypto/OlmDevice} olmDevice * - * @param {MatrixClient} baseApis * - * @param {object} devicesByUser - * map from userid to list of devices to ensure sessions for + * @param devicesByUser - map from userid to list of devices to ensure sessions for * - * @return {Promise} resolves to an array. The first element of the array is a + * @returns resolves to an array. The first element of the array is a * a map of user IDs to arrays of deviceInfo, representing the devices that * don't have established olm sessions. The second element of the array is - * a map from userId to deviceId to {@link module:crypto~OlmSessionResult} + * a map from userId to deviceId to {@link OlmSessionResult} */ async function getExistingOlmSessions(olmDevice, baseApis, devicesByUser) { - const devicesWithoutSession = {}; - const sessions = {}; + // map user Id → DeviceInfo[] + const devicesWithoutSession = new _utils.MapWithDefault(() => []); + // map user Id → device Id → IExistingOlmSession + const sessions = new _utils.MapWithDefault(() => new Map()); const promises = []; - for (const [userId, devices] of Object.entries(devicesByUser)) { for (const deviceInfo of devices) { const deviceId = deviceInfo.deviceId; const key = deviceInfo.getIdentityKey(); promises.push((async () => { const sessionId = await olmDevice.getSessionIdForDevice(key, true); - if (sessionId === null) { - devicesWithoutSession[userId] = devicesWithoutSession[userId] || []; - devicesWithoutSession[userId].push(deviceInfo); + devicesWithoutSession.getOrCreate(userId).push(deviceInfo); } else { - sessions[userId] = sessions[userId] || {}; - sessions[userId][deviceId] = { + sessions.getOrCreate(userId).set(deviceId, { device: deviceInfo, sessionId: sessionId - }; + }); } })()); } } - await Promise.all(promises); return [devicesWithoutSession, sessions]; } + /** * Try to make sure we have established olm sessions for the given devices. * - * @param {module:crypto/OlmDevice} olmDevice - * - * @param {MatrixClient} baseApis - * - * @param {object} devicesByUser - * map from userid to list of devices to ensure sessions for + * @param devicesByUser - map from userid to list of devices to ensure sessions for * - * @param {boolean} [force=false] If true, establish a new session even if one + * @param force - If true, establish a new session even if one * already exists. * - * @param {Number} [otkTimeout] The timeout in milliseconds when requesting + * @param otkTimeout - The timeout in milliseconds when requesting * one-time keys for establishing new olm sessions. * - * @param {Array} [failedServers] An array to fill with remote servers that + * @param failedServers - An array to fill with remote servers that * failed to respond to one-time-key requests. * - * @param {Logger} [log] A possibly customised log + * @param log - A possibly customised log * - * @return {Promise} resolves once the sessions are complete, to + * @returns resolves once the sessions are complete, to * an Object mapping from userId to deviceId to - * {@link module:crypto~OlmSessionResult} + * {@link OlmSessionResult} */ - - async function ensureOlmSessionsForDevices(olmDevice, baseApis, devicesByUser, force = false, otkTimeout, failedServers, log = _logger.logger) { - if (typeof force === "number") { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - backwards compatibility - log = failedServers; // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - backwards compatibility - - failedServers = otkTimeout; - otkTimeout = force; - force = false; - } - - const devicesWithoutSession = [// [userId, deviceId], ... + const devicesWithoutSession = [ + // [userId, deviceId], ... ]; - const result = {}; - const resolveSession = {}; // Mark all sessions this task intends to update as in progress. It is + // map user Id → device Id → IExistingOlmSession + const result = new Map(); + // map device key → resolve session fn + const resolveSession = new Map(); + + // Mark all sessions this task intends to update as in progress. It is // important to do this for all devices this task cares about in a single // synchronous operation, as otherwise it is possible to have deadlocks // where multiple tasks wait indefinitely on another task to update some set // of common devices. - - for (const [, devices] of Object.entries(devicesByUser)) { + for (const devices of devicesByUser.values()) { for (const deviceInfo of devices) { const key = deviceInfo.getIdentityKey(); - if (key === olmDevice.deviceCurve25519Key) { // We don't start sessions with ourself, so there's no need to // mark it in progress. continue; } - if (!olmDevice.sessionsInProgress[key]) { // pre-emptively mark the session as in-progress to avoid race // conditions. If we find that we already have a session, then // we'll resolve olmDevice.sessionsInProgress[key] = new Promise(resolve => { - resolveSession[key] = v => { + resolveSession.set(key, v => { delete olmDevice.sessionsInProgress[key]; resolve(v); - }; + }); }); } } } - - for (const [userId, devices] of Object.entries(devicesByUser)) { - result[userId] = {}; - + for (const [userId, devices] of devicesByUser) { + const resultDevices = new Map(); + result.set(userId, resultDevices); for (const deviceInfo of devices) { const deviceId = deviceInfo.deviceId; const key = deviceInfo.getIdentityKey(); - if (key === olmDevice.deviceCurve25519Key) { // We should never be trying to start a session with ourself. // Apart from talking to yourself being the first sign of madness, @@ -261,172 +209,134 @@ // new chain when this side has an active sender chain. // If you see this message being logged in the wild, we should find // the thing that is trying to send Olm messages to itself and fix it. - log.info("Attempted to start session with ourself! Ignoring"); // We must fill in the section in the return value though, as callers + log.info("Attempted to start session with ourself! Ignoring"); + // We must fill in the section in the return value though, as callers // expect it to be there. - - result[userId][deviceId] = { + resultDevices.set(deviceId, { device: deviceInfo, sessionId: null - }; + }); continue; } - const forWhom = `for ${key} (${userId}:${deviceId})`; - const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession[key], log); - - if (sessionId !== null && resolveSession[key]) { + const sessionId = await olmDevice.getSessionIdForDevice(key, !!resolveSession.get(key), log); + const resolveSessionFn = resolveSession.get(key); + if (sessionId !== null && resolveSessionFn) { // we found a session, but we had marked the session as // in-progress, so resolve it now, which will unmark it and // unblock anything that was waiting - resolveSession[key](); + resolveSessionFn(); } - if (sessionId === null || force) { if (force) { log.info(`Forcing new Olm session ${forWhom}`); } else { log.info(`Making new Olm session ${forWhom}`); } - devicesWithoutSession.push([userId, deviceId]); } - - result[userId][deviceId] = { + resultDevices.set(deviceId, { device: deviceInfo, sessionId: sessionId - }; + }); } } - if (devicesWithoutSession.length === 0) { return result; } - const oneTimeKeyAlgorithm = "signed_curve25519"; let res; let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`; - try { log.debug(`Claiming ${taskDetail}`); res = await baseApis.claimOneTimeKeys(devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout); log.debug(`Claimed ${taskDetail}`); } catch (e) { - for (const resolver of Object.values(resolveSession)) { + for (const resolver of resolveSession.values()) { resolver(); } - log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession); throw e; } - if (failedServers && "failures" in res) { failedServers.push(...Object.keys(res.failures)); } - const otkResult = res.one_time_keys || {}; const promises = []; - - for (const [userId, devices] of Object.entries(devicesByUser)) { + for (const [userId, devices] of devicesByUser) { const userRes = otkResult[userId] || {}; - - for (let j = 0; j < devices.length; j++) { - const deviceInfo = devices[j]; + for (const deviceInfo of devices) { const deviceId = deviceInfo.deviceId; const key = deviceInfo.getIdentityKey(); - if (key === olmDevice.deviceCurve25519Key) { // We've already logged about this above. Skip here too // otherwise we'll log saying there are no one-time keys // which will be confusing. continue; } - - if (result[userId][deviceId].sessionId && !force) { + if (result.get(userId)?.get(deviceId)?.sessionId && !force) { // we already have a result for this device continue; } - const deviceRes = userRes[deviceId] || {}; let oneTimeKey = null; - for (const keyId in deviceRes) { if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) { oneTimeKey = deviceRes[keyId]; } } - if (!oneTimeKey) { log.warn(`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` + `for device ${userId}:${deviceId}`); - - if (resolveSession[key]) { - resolveSession[key](); - } - + resolveSession.get(key)?.(); continue; } - promises.push(_verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo).then(sid => { - if (resolveSession[key]) { - resolveSession[key](sid); - } - - result[userId][deviceId].sessionId = sid; + resolveSession.get(key)?.(sid ?? undefined); + const deviceInfo = result.get(userId)?.get(deviceId); + if (deviceInfo) deviceInfo.sessionId = sid; }, e => { - if (resolveSession[key]) { - resolveSession[key](); - } - + resolveSession.get(key)?.(); throw e; })); } } - taskDetail = `Olm sessions for ${promises.length} devices`; log.debug(`Starting ${taskDetail}`); await Promise.all(promises); log.debug(`Started ${taskDetail}`); return result; } - async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) { const deviceId = deviceInfo.deviceId; - try { await verifySignature(olmDevice, oneTimeKey, userId, deviceId, deviceInfo.getFingerprint()); } catch (e) { _logger.logger.error("Unable to verify signature on one-time key for device " + userId + ":" + deviceId + ":", e); - return null; } - let sid; - try { sid = await olmDevice.createOutboundSession(deviceInfo.getIdentityKey(), oneTimeKey.key); } catch (e) { // possibly a bad key _logger.logger.error("Error starting olm session with device " + userId + ":" + deviceId + ": " + e); - return null; } - _logger.logger.log("Started new olm sessionid " + sid + " for device " + userId + ":" + deviceId); - return sid; } - /** * Verify the signature on an object * - * @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op + * @param olmDevice - olm wrapper to use for verify op * - * @param {Object} obj object to check signature on. + * @param obj - object to check signature on. * - * @param {string} signingUserId ID of the user whose signature should be checked + * @param signingUserId - ID of the user whose signature should be checked * - * @param {string} signingDeviceId ID of the device whose signature should be checked + * @param signingDeviceId - ID of the device whose signature should be checked * - * @param {string} signingKey base64-ed ed25519 public key + * @param signingKey - base64-ed ed25519 public key * * Returns a promise which resolves (to undefined) if the the signature is good, * or rejects with an Error if it is bad. @@ -436,86 +346,72 @@ const signatures = obj.signatures || {}; const userSigs = signatures[signingUserId] || {}; const signature = userSigs[signKeyId]; - if (!signature) { throw Error("No signature"); - } // prepare the canonical json: remove unsigned and signatures, and stringify with anotherjson - + } + // prepare the canonical json: remove unsigned and signatures, and stringify with anotherjson const mangledObj = Object.assign({}, obj); - if ("unsigned" in mangledObj) { delete mangledObj.unsigned; } - delete mangledObj.signatures; - const json = _anotherJson.default.stringify(mangledObj); - olmDevice.verifySignature(signingKey, json, signature); } + /** * Sign a JSON object using public key cryptography - * @param {Object} obj Object to sign. The object will be modified to include + * @param obj - Object to sign. The object will be modified to include * the new signature - * @param {Olm.PkSigning|Uint8Array} key the signing object or the private key + * @param key - the signing object or the private key * seed - * @param {string} userId The user ID who owns the signing key - * @param {string} pubKey The public key (ignored if key is a seed) - * @returns {string} the signature for the object + * @param userId - The user ID who owns the signing key + * @param pubKey - The public key (ignored if key is a seed) + * @returns the signature for the object */ - - function pkSign(obj, key, userId, pubKey) { let createdKey = false; - if (key instanceof Uint8Array) { const keyObj = new global.Olm.PkSigning(); pubKey = keyObj.init_with_seed(key); key = keyObj; createdKey = true; } - const sigs = obj.signatures || {}; delete obj.signatures; const unsigned = obj.unsigned; if (obj.unsigned) delete obj.unsigned; - try { const mysigs = sigs[userId] || {}; sigs[userId] = mysigs; - return mysigs['ed25519:' + pubKey] = key.sign(_anotherJson.default.stringify(obj)); + return mysigs["ed25519:" + pubKey] = key.sign(_anotherJson.default.stringify(obj)); } finally { obj.signatures = sigs; if (unsigned) obj.unsigned = unsigned; - if (createdKey) { key.free(); } } } + /** * Verify a signed JSON object - * @param {Object} obj Object to verify - * @param {string} pubKey The public key to use to verify - * @param {string} userId The user ID who signed the object + * @param obj - Object to verify + * @param pubKey - The public key to use to verify + * @param userId - The user ID who signed the object */ - - function pkVerify(obj, pubKey, userId) { const keyId = "ed25519:" + pubKey; - if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) { throw new Error("No signature"); } - const signature = obj.signatures[userId][keyId]; const util = new global.Olm.Utility(); const sigs = obj.signatures; delete obj.signatures; const unsigned = obj.unsigned; if (obj.unsigned) delete obj.unsigned; - try { util.ed25519_verify(pubKey, _anotherJson.default.stringify(obj), signature); } finally { @@ -524,53 +420,45 @@ util.free(); } } + /** * Check that an event was encrypted using olm. */ - - function isOlmEncrypted(event) { if (!event.getSenderKey()) { _logger.logger.error("Event has no sender key (not encrypted?)"); - return false; } - if (event.getWireType() !== _event.EventType.RoomMessageEncrypted || !["m.olm.v1.curve25519-aes-sha2"].includes(event.getWireContent().algorithm)) { _logger.logger.error("Event was not encrypted using an appropriate algorithm"); - return false; } - return true; } + /** * Encode a typed array of uint8 as base64. - * @param {Uint8Array} uint8Array The data to encode. - * @return {string} The base64. + * @param uint8Array - The data to encode. + * @returns The base64. */ - - function encodeBase64(uint8Array) { return Buffer.from(uint8Array).toString("base64"); } + /** * Encode a typed array of uint8 as unpadded base64. - * @param {Uint8Array} uint8Array The data to encode. - * @return {string} The unpadded base64. + * @param uint8Array - The data to encode. + * @returns The unpadded base64. */ - - function encodeUnpaddedBase64(uint8Array) { - return encodeBase64(uint8Array).replace(/=+$/g, ''); + return encodeBase64(uint8Array).replace(/=+$/g, ""); } + /** * Decode a base64 string to a typed array of uint8. - * @param {string} base64 The base64 to decode. - * @return {Uint8Array} The decoded data. + * @param base64 - The base64 to decode. + * @returns The decoded data. */ - - function decodeBase64(base64) { return Buffer.from(base64, "base64"); } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OutgoingRoomKeyRequestManager.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OutgoingRoomKeyRequestManager.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OutgoingRoomKeyRequestManager.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/OutgoingRoomKeyRequestManager.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,27 +4,31 @@ value: true }); exports.RoomKeyRequestState = exports.OutgoingRoomKeyRequestManager = void 0; - +var _uuid = require("uuid"); var _logger = require("../logger"); - var _event = require("../@types/event"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +var _utils = require("../utils"); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * Internal module. Management of outgoing room key requests. * * See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ * for draft documentation on what we're supposed to be implementing here. - * - * @module */ + // delay between deciding we want some keys, and sending out the request, to // allow for (a) it turning up anyway, (b) grouping requests together const SEND_KEY_REQUESTS_DELAY_MS = 500; -/** possible states for a room key request + +/** + * possible states for a room key request * * The state machine looks like: + * ``` * * | (cancellation sent) * | .-------------------------------------------------. @@ -47,63 +51,48 @@ * | (cancellation sent) | * V | * (deleted) <---------------------------+ - * - * @enum {number} + * ``` */ - let RoomKeyRequestState; exports.RoomKeyRequestState = RoomKeyRequestState; - (function (RoomKeyRequestState) { RoomKeyRequestState[RoomKeyRequestState["Unsent"] = 0] = "Unsent"; RoomKeyRequestState[RoomKeyRequestState["Sent"] = 1] = "Sent"; RoomKeyRequestState[RoomKeyRequestState["CancellationPending"] = 2] = "CancellationPending"; RoomKeyRequestState[RoomKeyRequestState["CancellationPendingAndWillResend"] = 3] = "CancellationPendingAndWillResend"; })(RoomKeyRequestState || (exports.RoomKeyRequestState = RoomKeyRequestState = {})); - class OutgoingRoomKeyRequestManager { // handle for the delayed call to sendOutgoingRoomKeyRequests. Non-null // if the callback has been set, or if it is still running. + // sanity check to ensure that we don't end up with two concurrent runs // of sendOutgoingRoomKeyRequests + constructor(baseApis, deviceId, cryptoStore) { this.baseApis = baseApis; this.deviceId = deviceId; this.cryptoStore = cryptoStore; - - _defineProperty(this, "sendOutgoingRoomKeyRequestsTimer", null); - + _defineProperty(this, "sendOutgoingRoomKeyRequestsTimer", void 0); _defineProperty(this, "sendOutgoingRoomKeyRequestsRunning", false); - - _defineProperty(this, "clientRunning", false); + _defineProperty(this, "clientRunning", true); } - /** - * Called when the client is started. Sets background processes running. - */ - - start() { - this.clientRunning = true; - } /** * Called when the client is stopped. Stops any running background processes. */ - - stop() { - _logger.logger.log('stopping OutgoingRoomKeyRequestManager'); // stop the timer on the next run - - + _logger.logger.log("stopping OutgoingRoomKeyRequestManager"); + // stop the timer on the next run this.clientRunning = false; } + /** * Send any requests that have been queued */ - - sendQueuedRequests() { this.startTimer(); } + /** * Queue up a room key request, if we haven't already queued or sent one. * @@ -112,20 +101,15 @@ * Otherwise, a request is added to the pending list, and a job is started * in the background to send it. * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * @param {Array<{userId: string, deviceId: string}>} recipients - * @param {boolean} resend whether to resend the key request if there is + * @param resend - whether to resend the key request if there is * already one * - * @returns {Promise} resolves when the request has been added to the + * @returns resolves when the request has been added to the * pending list (or we have established that a similar request already * exists) */ - - async queueRoomKeyRequest(requestBody, recipients, resend = false) { const req = await this.cryptoStore.getOutgoingRoomKeyRequest(requestBody); - if (!req) { await this.cryptoStore.getOrAddOutgoingRoomKeyRequest({ requestBody: requestBody, @@ -139,7 +123,6 @@ case RoomKeyRequestState.Unsent: // nothing to do here, since we're going to send a request anyways return; - case RoomKeyRequestState.CancellationPending: { // existing request is about to be cancelled. If we want to @@ -152,7 +135,6 @@ }); break; } - case RoomKeyRequestState.Sent: { // a request has already been sent. If we don't want to @@ -167,14 +149,15 @@ // the request gets sent requestTxnId: this.baseApis.makeTxnId() }); - if (!updatedReq) { // updateOutgoingRoomKeyRequest couldn't find the request // in state ROOM_KEY_REQUEST_STATES.SENT, so we must have // raced with another tab to mark the request cancelled. // Try again, to make sure the request is resent. return this.queueRoomKeyRequest(requestBody, recipients, resend); - } // We don't want to wait for the timer, so we send it + } + + // We don't want to wait for the timer, so we send it // immediately. (We might actually end up racing with the timer, // but that's ok: even if we make the request twice, we'll do it // with the same transaction_id, so only one message will get @@ -183,60 +166,53 @@ // (We also don't want to wait for the response from the server // here, as it will slow down processing of received keys if we // do.) - - try { await this.sendOutgoingRoomKeyRequestCancellation(updatedReq, true); } catch (e) { _logger.logger.error("Error sending room key request cancellation;" + " will retry later.", e); - } // The request has transitioned from + } + // The request has transitioned from // CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We // still need to resend the request which is now UNSENT, so // start the timer if it isn't already started. - } break; } - default: - throw new Error('unhandled state: ' + req.state); + throw new Error("unhandled state: " + req.state); } } } + /** * Cancel room key requests, if any match the given requestBody * - * @param {module:crypto~RoomKeyRequestBody} requestBody * - * @returns {Promise} resolves when the request has been updated in our + * @returns resolves when the request has been updated in our * pending list. */ - - cancelRoomKeyRequest(requestBody) { return this.cryptoStore.getOutgoingRoomKeyRequest(requestBody).then(req => { if (!req) { // no request was made for this key return; } - switch (req.state) { case RoomKeyRequestState.CancellationPending: case RoomKeyRequestState.CancellationPendingAndWillResend: // nothing to do here return; - case RoomKeyRequestState.Unsent: // just delete it + // FIXME: ghahah we may have attempted to send it, and // not yet got a successful response. So the server // may have seen it, so we still need to send a cancellation // in that case :/ - _logger.logger.log('deleting unnecessary room key request for ' + stringifyRequestBody(requestBody)); + _logger.logger.log("deleting unnecessary room key request for " + stringifyRequestBody(requestBody)); return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.Unsent); - case RoomKeyRequestState.Sent: { // send a cancellation. @@ -251,10 +227,11 @@ // the request cancelled. There is no point in // sending another cancellation since the other tab // will do it. - _logger.logger.log('Tried to cancel room key request for ' + stringifyRequestBody(requestBody) + ' but it was already cancelled in another tab'); - + _logger.logger.log("Tried to cancel room key request for " + stringifyRequestBody(requestBody) + " but it was already cancelled in another tab"); return; - } // We don't want to wait for the timer, so we send it + } + + // We don't want to wait for the timer, so we send it // immediately. (We might actually end up racing with the timer, // but that's ok: even if we make the request twice, we'll do it // with the same transaction_id, so only one message will get @@ -263,64 +240,55 @@ // (We also don't want to wait for the response from the server // here, as it will slow down processing of received keys if we // do.) - - this.sendOutgoingRoomKeyRequestCancellation(updatedReq).catch(e => { _logger.logger.error("Error sending room key request cancellation;" + " will retry later.", e); - this.startTimer(); }); }); } - default: - throw new Error('unhandled state: ' + req.state); + throw new Error("unhandled state: " + req.state); } }); } + /** * Look for room key requests by target device and state * - * @param {string} userId Target user ID - * @param {string} deviceId Target device ID + * @param userId - Target user ID + * @param deviceId - Target device ID * - * @return {Promise} resolves to a list of all the - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to a list of all the {@link OutgoingRoomKeyRequest} */ - - getOutgoingSentRoomKeyRequest(userId, deviceId) { return this.cryptoStore.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, [RoomKeyRequestState.Sent]); } + /** * Find anything in `sent` state, and kick it around the loop again. * This is intended for situations where something substantial has changed, and we * don't really expect the other end to even care about the cancellation. * For example, after initialization or self-verification. - * @return {Promise} An array of `queueRoomKeyRequest` outputs. + * @returns An array of `queueRoomKeyRequest` outputs. */ - - async cancelAndResendAllOutgoingRequests() { const outgoings = await this.cryptoStore.getAllOutgoingRoomKeyRequestsByState(RoomKeyRequestState.Sent); return Promise.all(outgoings.map(({ requestBody, recipients }) => this.queueRoomKeyRequest(requestBody, recipients, true))); - } // start the background timer to send queued requests, if the timer isn't - // already running - + } + // start the background timer to send queued requests, if the timer isn't + // already running startTimer() { if (this.sendOutgoingRoomKeyRequestsTimer) { return; } - const startSendingOutgoingRoomKeyRequests = () => { if (this.sendOutgoingRoomKeyRequestsRunning) { throw new Error("RoomKeyRequestSend already in progress!"); } - this.sendOutgoingRoomKeyRequestsRunning = true; this.sendOutgoingRoomKeyRequests().finally(() => { this.sendOutgoingRoomKeyRequestsRunning = false; @@ -330,56 +298,46 @@ _logger.logger.warn(`error in OutgoingRoomKeyRequestManager: ${e}`); }); }; - this.sendOutgoingRoomKeyRequestsTimer = setTimeout(startSendingOutgoingRoomKeyRequests, SEND_KEY_REQUESTS_DELAY_MS); - } // look for and send any queued requests. Runs itself recursively until + } + + // look for and send any queued requests. Runs itself recursively until // there are no more requests, or there is an error (in which case, the // timer will be restarted before the promise resolves). - - - sendOutgoingRoomKeyRequests() { + async sendOutgoingRoomKeyRequests() { if (!this.clientRunning) { - this.sendOutgoingRoomKeyRequestsTimer = null; - return Promise.resolve(); + this.sendOutgoingRoomKeyRequestsTimer = undefined; + return; } - - return this.cryptoStore.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.CancellationPending, RoomKeyRequestState.CancellationPendingAndWillResend, RoomKeyRequestState.Unsent]).then(req => { - if (!req) { - this.sendOutgoingRoomKeyRequestsTimer = null; - return; - } - - let prom; - + const req = await this.cryptoStore.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.CancellationPending, RoomKeyRequestState.CancellationPendingAndWillResend, RoomKeyRequestState.Unsent]); + if (!req) { + this.sendOutgoingRoomKeyRequestsTimer = undefined; + return; + } + try { switch (req.state) { case RoomKeyRequestState.Unsent: - prom = this.sendOutgoingRoomKeyRequest(req); + await this.sendOutgoingRoomKeyRequest(req); break; - case RoomKeyRequestState.CancellationPending: - prom = this.sendOutgoingRoomKeyRequestCancellation(req); + await this.sendOutgoingRoomKeyRequestCancellation(req); break; - case RoomKeyRequestState.CancellationPendingAndWillResend: - prom = this.sendOutgoingRoomKeyRequestCancellation(req, true); + await this.sendOutgoingRoomKeyRequestCancellation(req, true); break; } - return prom.then(() => { - // go around the loop again - return this.sendOutgoingRoomKeyRequests(); - }).catch(e => { - _logger.logger.error("Error sending room key request; will retry later.", e); - - this.sendOutgoingRoomKeyRequestsTimer = null; - }); - }); - } // given a RoomKeyRequest, send it and update the request record - + // go around the loop again + return this.sendOutgoingRoomKeyRequests(); + } catch (e) { + _logger.logger.error("Error sending room key request; will retry later.", e); + this.sendOutgoingRoomKeyRequestsTimer = undefined; + } + } + // given a RoomKeyRequest, send it and update the request record sendOutgoingRoomKeyRequest(req) { _logger.logger.log(`Requesting keys for ${stringifyRequestBody(req.requestBody)}` + ` from ${stringifyRecipientList(req.recipients)}` + `(id ${req.requestId})`); - const requestMessage = { action: "request", requesting_device_id: this.deviceId, @@ -391,13 +349,12 @@ state: RoomKeyRequestState.Sent }); }); - } // Given a RoomKeyRequest, cancel it and delete the request record unless - // andResend is set, in which case transition to UNSENT. - + } + // Given a RoomKeyRequest, cancel it and delete the request record unless + // andResend is set, in which case transition to UNSENT. sendOutgoingRoomKeyRequestCancellation(req, andResend = false) { _logger.logger.log(`Sending cancellation for key request for ` + `${stringifyRequestBody(req.requestBody)} to ` + `${stringifyRecipientList(req.recipients)} ` + `(cancellation id ${req.cancellationTxnId})`); - const requestMessage = { action: "request_cancellation", requesting_device_id: this.deviceId, @@ -410,36 +367,28 @@ state: RoomKeyRequestState.Unsent }); } - return this.cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId, RoomKeyRequestState.CancellationPending); }); - } // send a RoomKeyRequest to a list of recipients - + } + // send a RoomKeyRequest to a list of recipients sendMessageToDevices(message, recipients, txnId) { - const contentMap = {}; - + const contentMap = new _utils.MapWithDefault(() => new Map()); for (const recip of recipients) { - if (!contentMap[recip.userId]) { - contentMap[recip.userId] = {}; - } - - contentMap[recip.userId][recip.deviceId] = message; + const userDeviceMap = contentMap.getOrCreate(recip.userId); + userDeviceMap.set(recip.deviceId, _objectSpread(_objectSpread({}, message), {}, { + [_event.ToDeviceMessageId]: (0, _uuid.v4)() + })); } - return this.baseApis.sendToDevice(_event.EventType.RoomKeyRequest, contentMap, txnId); } - } - exports.OutgoingRoomKeyRequestManager = OutgoingRoomKeyRequestManager; - function stringifyRequestBody(requestBody) { // we assume that the request is for megolm keys, which are identified by // room id and session id return requestBody.room_id + " / " + requestBody.session_id; } - function stringifyRecipientList(recipients) { - return '[' + recipients.map(r => `${r.userId}:${r.deviceId}`).join(",") + ']'; + return `[${recipients.map(r => `${r.userId}:${r.deviceId}`).join(",")}]`; } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/recoverykey.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/recoverykey.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/recoverykey.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/recoverykey.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,13 +5,9 @@ }); exports.decodeRecoveryKey = decodeRecoveryKey; exports.encodeRecoveryKey = encodeRecoveryKey; - var bs58 = _interopRequireWildcard(require("bs58")); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - /* Copyright 2018 New Vector Ltd @@ -27,46 +23,38 @@ See the License for the specific language governing permissions and limitations under the License. */ + // picked arbitrarily but to try & avoid clashing with any bitcoin ones // (which are also base58 encoded, but bitcoin's involve a lot more hashing) -const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; - +const OLM_RECOVERY_KEY_PREFIX = [0x8b, 0x01]; function encodeRecoveryKey(key) { const buf = Buffer.alloc(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1); buf.set(OLM_RECOVERY_KEY_PREFIX, 0); buf.set(key, OLM_RECOVERY_KEY_PREFIX.length); let parity = 0; - for (let i = 0; i < buf.length - 1; ++i) { parity ^= buf[i]; } - buf[buf.length - 1] = parity; const base58key = bs58.encode(buf); - return base58key.match(/.{1,4}/g).join(" "); + return base58key.match(/.{1,4}/g)?.join(" "); } - function decodeRecoveryKey(recoveryKey) { - const result = bs58.decode(recoveryKey.replace(/ /g, '')); + const result = bs58.decode(recoveryKey.replace(/ /g, "")); let parity = 0; - for (const b of result) { parity ^= b; } - if (parity !== 0) { throw new Error("Incorrect parity"); } - for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) { if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) { throw new Error("Incorrect prefix"); } } - if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) { throw new Error("Incorrect length"); } - return Uint8Array.from(result.slice(OLM_RECOVERY_KEY_PREFIX.length, OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH)); } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/RoomList.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/RoomList.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/RoomList.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/RoomList.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,50 +4,40 @@ value: true }); exports.RoomList = void 0; - var _indexeddbCryptoStore = require("./store/indexeddb-crypto-store"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* eslint-enable camelcase */ -/** - * @alias module:crypto/RoomList - */ class RoomList { // Object of roomId -> room e2e info object (body of the m.room.encryption event) + constructor(cryptoStore) { this.cryptoStore = cryptoStore; - _defineProperty(this, "roomEncryption", {}); } - async init() { - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ROOMS], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ROOMS], txn => { this.cryptoStore.getEndToEndRooms(txn, result => { this.roomEncryption = result; }); }); } - getRoomEncryption(roomId) { return this.roomEncryption[roomId] || null; } - isRoomEncrypted(roomId) { return Boolean(this.getRoomEncryption(roomId)); } - async setRoomEncryption(roomId, roomInfo) { // important that this happens before calling into the store // as it prevents the Crypto::setRoomEncryption from calling // this twice for consecutive m.room.encryption events this.roomEncryption[roomId] = roomInfo; - await this.cryptoStore.doTxn('readwrite', [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ROOMS], txn => { + await this.cryptoStore.doTxn("readwrite", [_indexeddbCryptoStore.IndexedDBCryptoStore.STORE_ROOMS], txn => { this.cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn); }); } - } - exports.RoomList = RoomList; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretStorage.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretStorage.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretStorage.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/SecretStorage.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,30 +4,25 @@ value: true }); exports.SecretStorage = exports.SECRET_STORAGE_ALGORITHM_V1_AES = void 0; - +var _uuid = require("uuid"); var _logger = require("../logger"); - var olmlib = _interopRequireWildcard(require("./olmlib")); - var _randomstring = require("../randomstring"); - var _aes = require("./aes"); - -var _matrix = require("../matrix"); - +var _client = require("../client"); +var _utils = require("../utils"); +var _event = require("../@types/event"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -const SECRET_STORAGE_ALGORITHM_V1_AES = "m.secret_storage.v1.aes-hmac-sha2"; // Some of the key functions use a tuple and some use an object... - +// Some of the key functions use a tuple and some use an object... exports.SECRET_STORAGE_ALGORITHM_V1_AES = SECRET_STORAGE_ALGORITHM_V1_AES; - /** * Implements Secure Secret Storage and Sharing (MSC1946) - * @module crypto/SecretStorage */ class SecretStorage { // In it's pure javascript days, this was relying on some proper Javascript-style @@ -42,64 +37,55 @@ this.accountDataAdapter = accountDataAdapter; this.cryptoCallbacks = cryptoCallbacks; this.baseApis = baseApis; - _defineProperty(this, "requests", new Map()); } - async getDefaultKeyId() { - const defaultKey = await this.accountDataAdapter.getAccountDataFromServer('m.secret_storage.default_key'); + const defaultKey = await this.accountDataAdapter.getAccountDataFromServer("m.secret_storage.default_key"); if (!defaultKey) return null; return defaultKey.key; } - setDefaultKeyId(keyId) { return new Promise((resolve, reject) => { const listener = ev => { - if (ev.getType() === 'm.secret_storage.default_key' && ev.getContent().key === keyId) { - this.accountDataAdapter.removeListener(_matrix.ClientEvent.AccountData, listener); + if (ev.getType() === "m.secret_storage.default_key" && ev.getContent().key === keyId) { + this.accountDataAdapter.removeListener(_client.ClientEvent.AccountData, listener); resolve(); } }; - - this.accountDataAdapter.on(_matrix.ClientEvent.AccountData, listener); - this.accountDataAdapter.setAccountData('m.secret_storage.default_key', { + this.accountDataAdapter.on(_client.ClientEvent.AccountData, listener); + this.accountDataAdapter.setAccountData("m.secret_storage.default_key", { key: keyId }).catch(e => { - this.accountDataAdapter.removeListener(_matrix.ClientEvent.AccountData, listener); + this.accountDataAdapter.removeListener(_client.ClientEvent.AccountData, listener); reject(e); }); }); } + /** * Add a key for encrypting secrets. * - * @param {string} algorithm the algorithm used by the key. - * @param {object} opts the options for the algorithm. The properties used + * @param algorithm - the algorithm used by the key. + * @param opts - the options for the algorithm. The properties used * depend on the algorithm given. - * @param {string} [keyId] the ID of the key. If not given, a random + * @param keyId - the ID of the key. If not given, a random * ID will be generated. * - * @return {object} An object with: - * keyId: {string} the ID of the key - * keyInfo: {object} details about the key (iv, mac, passphrase) + * @returns An object with: + * keyId: the ID of the key + * keyInfo: details about the key (iv, mac, passphrase) */ - - - async addKey(algorithm, opts, keyId) { + async addKey(algorithm, opts = {}, keyId) { const keyInfo = { algorithm }; - if (!opts) opts = {}; - if (opts.name) { keyInfo.name = opts.name; } - if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { if (opts.passphrase) { keyInfo.passphrase = opts.passphrase; } - if (opts.key) { const { iv, @@ -111,71 +97,64 @@ } else { throw new Error(`Unknown key algorithm ${algorithm}`); } - if (!keyId) { do { keyId = (0, _randomstring.randomString)(32); } while (await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`)); } - await this.accountDataAdapter.setAccountData(`m.secret_storage.key.${keyId}`, keyInfo); return { keyId, keyInfo }; } + /** * Get the key information for a given ID. * - * @param {string} [keyId = default key's ID] The ID of the key to check + * @param keyId - The ID of the key to check * for. Defaults to the default key ID if not provided. - * @returns {Array?} If the key was found, the return value is an array of + * @returns If the key was found, the return value is an array of * the form [keyId, keyInfo]. Otherwise, null is returned. * XXX: why is this an array when addKey returns an object? */ - - async getKey(keyId) { if (!keyId) { keyId = await this.getDefaultKeyId(); } - if (!keyId) { return null; } - const keyInfo = await this.accountDataAdapter.getAccountDataFromServer("m.secret_storage.key." + keyId); return keyInfo ? [keyId, keyInfo] : null; } + /** * Check whether we have a key with a given ID. * - * @param {string} [keyId = default key's ID] The ID of the key to check + * @param keyId - The ID of the key to check * for. Defaults to the default key ID if not provided. - * @return {boolean} Whether we have the key. + * @returns Whether we have the key. */ - - async hasKey(keyId) { return Boolean(await this.getKey(keyId)); } + /** * Check whether a key matches what we expect based on the key info * - * @param {Uint8Array} key the key to check - * @param {object} info the key info + * @param key - the key to check + * @param info - the key info * - * @return {boolean} whether or not the key matches + * @returns whether or not the key matches */ - - async checkKey(key, info) { if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { if (info.mac) { const { mac } = await (0, _aes.calculateKeyCheck)(key, info.iv); - return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, ''); + return info.mac.replace(/=+$/g, "") === mac.replace(/=+$/g, ""); } else { // if we have no information, we have to assume the key is right return true; @@ -184,42 +163,35 @@ throw new Error("Unknown algorithm"); } } + /** * Store an encrypted secret on the server * - * @param {string} name The name of the secret - * @param {string} secret The secret contents. - * @param {Array} keys The IDs of the keys to use to encrypt the secret + * @param name - The name of the secret + * @param secret - The secret contents. + * @param keys - The IDs of the keys to use to encrypt the secret * or null/undefined to use the default key. */ - - async store(name, secret, keys) { const encrypted = {}; - if (!keys) { const defaultKeyId = await this.getDefaultKeyId(); - if (!defaultKeyId) { throw new Error("No keys specified and no default key present"); } - keys = [defaultKeyId]; } - if (keys.length === 0) { throw new Error("Zero keys given to encrypt with!"); } - for (const keyId of keys) { // get key information from key storage const keyInfo = await this.accountDataAdapter.getAccountDataFromServer("m.secret_storage.key." + keyId); - if (!keyInfo) { throw new Error("Unknown key: " + keyId); - } // encrypt secret, based on the algorithm - + } + // encrypt secret, based on the algorithm if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { const keys = { [keyId]: keyInfo @@ -227,126 +199,102 @@ const [, encryption] = await this.getSecretStorageKey(keys, name); encrypted[keyId] = await encryption.encrypt(secret); } else { - _logger.logger.warn("unknown algorithm for secret storage key " + keyId + ": " + keyInfo.algorithm); // do nothing if we don't understand the encryption algorithm - + _logger.logger.warn("unknown algorithm for secret storage key " + keyId + ": " + keyInfo.algorithm); + // do nothing if we don't understand the encryption algorithm } - } // save encrypted secret - + } + // save encrypted secret await this.accountDataAdapter.setAccountData(name, { encrypted }); } + /** * Get a secret from storage. * - * @param {string} name the name of the secret + * @param name - the name of the secret * - * @return {string} the contents of the secret + * @returns the contents of the secret */ - - async get(name) { const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); - if (!secretInfo) { return; } - if (!secretInfo.encrypted) { throw new Error("Content is not encrypted!"); - } // get possible keys to decrypt - + } + // get possible keys to decrypt const keys = {}; - for (const keyId of Object.keys(secretInfo.encrypted)) { // get key information from key storage const keyInfo = await this.accountDataAdapter.getAccountDataFromServer("m.secret_storage.key." + keyId); - const encInfo = secretInfo.encrypted[keyId]; // only use keys we understand the encryption algorithm of - + const encInfo = secretInfo.encrypted[keyId]; + // only use keys we understand the encryption algorithm of if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { if (encInfo.iv && encInfo.ciphertext && encInfo.mac) { keys[keyId] = keyInfo; } } } - if (Object.keys(keys).length === 0) { throw new Error(`Could not decrypt ${name} because none of ` + `the keys it is encrypted with are for a supported algorithm`); } - let keyId; - let decryption; - - try { - // fetch private key from app - [keyId, decryption] = await this.getSecretStorageKey(keys, name); - const encInfo = secretInfo.encrypted[keyId]; // We don't actually need the decryption object if it's a passthrough - // since we just want to return the key itself. It must be base64 - // encoded, since this is how a key would normally be stored. - - if (encInfo.passthrough) return (0, olmlib.encodeBase64)(decryption.get_private_key()); - return decryption.decrypt(encInfo); - } finally { - if (decryption && decryption.free) decryption.free(); - } + // fetch private key from app + const [keyId, decryption] = await this.getSecretStorageKey(keys, name); + const encInfo = secretInfo.encrypted[keyId]; + return decryption.decrypt(encInfo); } + /** * Check if a secret is stored on the server. * - * @param {string} name the name of the secret + * @param name - the name of the secret * - * @return {object?} map of key name to key info the secret is encrypted + * @returns map of key name to key info the secret is encrypted * with, or null if it is not present or not encrypted with a trusted * key */ - - async isStored(name) { // check if secret exists const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); if (!secretInfo?.encrypted) return null; - const ret = {}; // filter secret encryption keys with supported algorithm + const ret = {}; + // filter secret encryption keys with supported algorithm for (const keyId of Object.keys(secretInfo.encrypted)) { // get key information from key storage const keyInfo = await this.accountDataAdapter.getAccountDataFromServer("m.secret_storage.key." + keyId); if (!keyInfo) continue; - const encInfo = secretInfo.encrypted[keyId]; // only use keys we understand the encryption algorithm of + const encInfo = secretInfo.encrypted[keyId]; + // only use keys we understand the encryption algorithm of if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { if (encInfo.iv && encInfo.ciphertext && encInfo.mac) { ret[keyId] = keyInfo; } } } - return Object.keys(ret).length ? ret : null; } + /** * Request a secret from another device * - * @param {string} name the name of the secret to request - * @param {string[]} devices the devices to request the secret from + * @param name - the name of the secret to request + * @param devices - the devices to request the secret from */ - - request(name, devices) { const requestId = this.baseApis.makeTxnId(); - let resolve; - let reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); + const deferred = (0, _utils.defer)(); this.requests.set(requestId, { name, devices, - resolve, - reject + deferred }); - const cancel = reason => { // send cancellation event const cancelData = { @@ -354,56 +302,46 @@ requesting_device_id: this.baseApis.deviceId, request_id: requestId }; - const toDevice = {}; - + const toDevice = new Map(); for (const device of devices) { - toDevice[device] = cancelData; + toDevice.set(device, cancelData); } + this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId(), toDevice]])); - this.baseApis.sendToDevice("m.secret.request", { - [this.baseApis.getUserId()]: toDevice - }); // and reject the promise so that anyone waiting on it will be + // and reject the promise so that anyone waiting on it will be // notified + deferred.reject(new Error(reason || "Cancelled")); + }; - reject(new Error(reason || "Cancelled")); - }; // send request to devices - - + // send request to devices const requestData = { name, action: "request", requesting_device_id: this.baseApis.deviceId, - request_id: requestId + request_id: requestId, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() }; - const toDevice = {}; - + const toDevice = new Map(); for (const device of devices) { - toDevice[device] = requestData; + toDevice.set(device, requestData); } - _logger.logger.info(`Request secret ${name} from ${devices}, id ${requestId}`); - - this.baseApis.sendToDevice("m.secret.request", { - [this.baseApis.getUserId()]: toDevice - }); + this.baseApis.sendToDevice("m.secret.request", new Map([[this.baseApis.getUserId(), toDevice]])); return { requestId, - promise, + promise: deferred.promise, cancel }; } - async onRequestReceived(event) { const sender = event.getSender(); const content = event.getContent(); - if (sender !== this.baseApis.getUserId() || !(content.name && content.action && content.requesting_device_id && content.request_id)) { // ignore requests from anyone else, for now return; } - - const deviceId = content.requesting_device_id; // check if it's a cancel - + const deviceId = content.requesting_device_id; + // check if it's a cancel if (content.action === "request_cancellation") { /* Looks like we intended to emit events when we got cancelations, but @@ -428,20 +366,16 @@ if (deviceId === this.baseApis.deviceId) { // no point in trying to send ourself the secret return; - } // check if we have the secret - + } + // check if we have the secret _logger.logger.info("received request for secret (" + sender + ", " + deviceId + ", " + content.request_id + ")"); - if (!this.cryptoCallbacks.onSecretRequested) { return; } - const secret = await this.cryptoCallbacks.onSecretRequested(sender, deviceId, content.request_id, content.name, this.baseApis.checkDeviceTrust(sender, deviceId)); - if (secret) { _logger.logger.info(`Preparing ${content.name} secret for ${deviceId}`); - const payload = { type: "m.secret.send", content: { @@ -452,110 +386,78 @@ const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this.baseApis.crypto.olmDevice.deviceCurve25519Key, - ciphertext: {} + ciphertext: {}, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() }; - await olmlib.ensureOlmSessionsForDevices(this.baseApis.crypto.olmDevice, this.baseApis, { - [sender]: [this.baseApis.getStoredDevice(sender, deviceId)] - }); + await olmlib.ensureOlmSessionsForDevices(this.baseApis.crypto.olmDevice, this.baseApis, new Map([[sender, [this.baseApis.getStoredDevice(sender, deviceId)]]])); await olmlib.encryptMessageForDevice(encryptedContent.ciphertext, this.baseApis.getUserId(), this.baseApis.deviceId, this.baseApis.crypto.olmDevice, sender, this.baseApis.getStoredDevice(sender, deviceId), payload); - const contentMap = { - [sender]: { - [deviceId]: encryptedContent - } - }; - + const contentMap = new Map([[sender, new Map([[deviceId, encryptedContent]])]]); _logger.logger.info(`Sending ${content.name} secret for ${deviceId}`); - this.baseApis.sendToDevice("m.room.encrypted", contentMap); } else { _logger.logger.info(`Request denied for ${content.name} secret for ${deviceId}`); } } } - onSecretReceived(event) { if (event.getSender() !== this.baseApis.getUserId()) { // we shouldn't be receiving secrets from anyone else, so ignore // because someone could be trying to send us bogus data return; } - if (!olmlib.isOlmEncrypted(event)) { _logger.logger.error("secret event not properly encrypted"); - return; } - const content = event.getContent(); - const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, content.sender_key); - + const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey(olmlib.OLM_ALGORITHM, event.getSenderKey() || ""); if (senderKeyUser !== event.getSender()) { _logger.logger.error("sending device does not belong to the user it claims to be from"); - return; } - _logger.logger.log("got secret share for request", content.request_id); - const requestControl = this.requests.get(content.request_id); - if (requestControl) { // make sure that the device that sent it is one of the devices that // we requested from const deviceInfo = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, event.getSenderKey()); - if (!deviceInfo) { _logger.logger.log("secret share from unknown device with key", event.getSenderKey()); - return; } - if (!requestControl.devices.includes(deviceInfo.deviceId)) { _logger.logger.log("unsolicited secret share from device", deviceInfo.deviceId); - return; - } // unsure that the sender is trusted. In theory, this check is + } + // unsure that the sender is trusted. In theory, this check is // unnecessary since we only accept secret shares from devices that // we requested from, but it doesn't hurt. - - const deviceTrust = this.baseApis.crypto.checkDeviceInfoTrust(event.getSender(), deviceInfo); - if (!deviceTrust.isVerified()) { _logger.logger.log("secret share from unverified device"); - return; } - _logger.logger.log(`Successfully received secret ${requestControl.name} ` + `from ${deviceInfo.deviceId}`); - - requestControl.resolve(content.secret); + requestControl.deferred.resolve(content.secret); } } - async getSecretStorageKey(keys, name) { if (!this.cryptoCallbacks.getSecretStorageKey) { throw new Error("No getSecretStorageKey callback supplied"); } - const returned = await this.cryptoCallbacks.getSecretStorageKey({ keys }, name); - if (!returned) { throw new Error("getSecretStorageKey callback returned falsey"); } - if (returned.length < 2) { throw new Error("getSecretStorageKey callback returned invalid data"); } - const [keyId, privateKey] = returned; - if (!keys[keyId]) { throw new Error("App returned unknown key from getSecretStorageKey!"); } - if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) { const decryption = { encrypt: function (secret) { @@ -570,7 +472,5 @@ throw new Error("Unknown key type: " + keys[keyId].algorithm); } } - } - exports.SecretStorage = SecretStorage; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store-backend.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store-backend.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store-backend.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store-backend.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,205 +5,173 @@ }); exports.VERSION = exports.Backend = void 0; exports.upgradeDatabase = upgradeDatabase; - var _logger = require("../../logger"); - var utils = _interopRequireWildcard(require("../../utils")); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -const VERSION = 11; -exports.VERSION = VERSION; +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const PROFILE_TRANSACTIONS = false; + /** * Implementation of a CryptoStore which is backed by an existing * IndexedDB connection. Generally you want IndexedDBCryptoStore * which connects to the database and defers to one of these. - * - * @implements {module:crypto/store/base~CryptoStore} */ - class Backend { /** - * @param {IDBDatabase} db */ constructor(db) { this.db = db; - _defineProperty(this, "nextTxnId", 0); - // make sure we close the db on `onversionchange` - otherwise // attempts to delete the database will block (and subsequent // attempts to re-create it will also block). db.onversionchange = () => { _logger.logger.log(`versionchange for indexeddb ${this.db.name}: closing`); - db.close(); }; } - async startup() { // No work to do, as the startup is done by the caller (e.g IndexedDBCryptoStore) // by passing us a ready IDBDatabase instance return this; } - async deleteAllData() { throw Error("This is not implemented, call IDBFactory::deleteDatabase(dbName) instead."); } + /** * Look for an existing outgoing room key request, and if none is found, * add a new one * - * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * @returns resolves to + * {@link OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ - - getOrAddOutgoingRoomKeyRequest(request) { const requestBody = request.requestBody; return new Promise((resolve, reject) => { const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite"); - txn.onerror = reject; // first see if we already have an entry for this request. + txn.onerror = reject; + // first see if we already have an entry for this request. this._getOutgoingRoomKeyRequest(txn, requestBody, existing => { if (existing) { // this entry matches the request - return it. _logger.logger.log(`already have key request outstanding for ` + `${requestBody.room_id} / ${requestBody.session_id}: ` + `not sending another`); - resolve(existing); return; - } // we got to the end of the list without finding a match - // - add the new request. - + } + // we got to the end of the list without finding a match + // - add the new request. _logger.logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id); - txn.oncomplete = () => { resolve(request); }; - const store = txn.objectStore("outgoingRoomKeyRequests"); store.add(request); }); }); } + /** * Look for an existing room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {Promise} resolves to the matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the matching + * {@link OutgoingRoomKeyRequest}, or null if * not found */ - - getOutgoingRoomKeyRequest(requestBody) { return new Promise((resolve, reject) => { const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly"); txn.onerror = reject; - this._getOutgoingRoomKeyRequest(txn, requestBody, existing => { resolve(existing); }); }); } + /** * look for an existing room key request in the db * - * @private - * @param {IDBTransaction} txn database transaction - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for - * @param {Function} callback function to call with the results of the + * @internal + * @param txn - database transaction + * @param requestBody - existing request to look for + * @param callback - function to call with the results of the * search. Either passed a matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * {@link OutgoingRoomKeyRequest}, or null if * not found. */ // eslint-disable-next-line @typescript-eslint/naming-convention - - _getOutgoingRoomKeyRequest(txn, requestBody, callback) { const store = txn.objectStore("outgoingRoomKeyRequests"); const idx = store.index("session"); const cursorReq = idx.openCursor([requestBody.room_id, requestBody.session_id]); - cursorReq.onsuccess = () => { const cursor = cursorReq.result; - if (!cursor) { // no match found callback(null); return; } - const existing = cursor.value; - if (utils.deepCompare(existing.requestBody, requestBody)) { // got a match callback(existing); return; - } // look at the next entry in the index - + } + // look at the next entry in the index cursor.continue(); }; } + /** * Look for room key requests by state * - * @param {Array} wantedStates list of acceptable states + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to the a - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the a + * {@link OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states. If there are multiple * requests in those states, an arbitrary one is chosen. */ - - getOutgoingRoomKeyRequestByState(wantedStates) { if (wantedStates.length === 0) { return Promise.resolve(null); - } // this is a bit tortuous because we need to make sure we do the lookup + } + + // this is a bit tortuous because we need to make sure we do the lookup // in a single transaction, to avoid having a race with the insertion // code. - // index into the wantedStates array - + // index into the wantedStates array let stateIndex = 0; let result; - function onsuccess() { const cursor = this.result; - if (cursor) { // got a match result = cursor.value; return; - } // try the next state in the list - + } + // try the next state in the list stateIndex++; - if (stateIndex >= wantedStates.length) { // no matches return; } - const wantedState = wantedStates[stateIndex]; const cursorReq = this.source.openCursor(wantedState); cursorReq.onsuccess = onsuccess; } - const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly"); const store = txn.objectStore("outgoingRoomKeyRequests"); const wantedState = wantedStates[stateIndex]; @@ -211,56 +179,44 @@ cursorReq.onsuccess = onsuccess; return promiseifyTxn(txn).then(() => result); } + /** * - * @param {Number} wantedState - * @return {Promise>} All elements in a given state + * @returns All elements in a given state */ - - getAllOutgoingRoomKeyRequestsByState(wantedState) { return new Promise((resolve, reject) => { const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly"); const store = txn.objectStore("outgoingRoomKeyRequests"); const index = store.index("state"); const request = index.getAll(wantedState); - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); }); } - getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { let stateIndex = 0; const results = []; - function onsuccess() { const cursor = this.result; - if (cursor) { const keyReq = cursor.value; - if (keyReq.recipients.some(recipient => recipient.userId === userId && recipient.deviceId === deviceId)) { results.push(keyReq); } - cursor.continue(); } else { // try the next state in the list stateIndex++; - if (stateIndex >= wantedStates.length) { // no matches return; } - const wantedState = wantedStates[stateIndex]; const cursorReq = this.source.openCursor(wantedState); cursorReq.onsuccess = onsuccess; } } - const txn = this.db.transaction("outgoingRoomKeyRequests", "readonly"); const store = txn.objectStore("outgoingRoomKeyRequests"); const wantedState = wantedStates[stateIndex]; @@ -268,89 +224,73 @@ cursorReq.onsuccess = onsuccess; return promiseifyTxn(txn).then(() => results); } + /** * Look for an existing room key request by id and state, and update it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in - * @param {Object} updates name/value map of updates to apply + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in + * @param updates - name/value map of updates to apply * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to + * {@link OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ - - updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { let result = null; - function onsuccess() { const cursor = this.result; - if (!cursor) { return; } - const data = cursor.value; - if (data.state != expectedState) { _logger.logger.warn(`Cannot update room key request from ${expectedState} ` + `as it was already updated to ${data.state}`); - return; } - Object.assign(data, updates); cursor.update(data); result = data; } - const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite"); const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId); cursorReq.onsuccess = onsuccess; return promiseifyTxn(txn).then(() => result); } + /** * Look for an existing room key request by id and state, and delete it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in * - * @returns {Promise} resolves once the operation is completed + * @returns resolves once the operation is completed */ - - deleteOutgoingRoomKeyRequest(requestId, expectedState) { const txn = this.db.transaction("outgoingRoomKeyRequests", "readwrite"); const cursorReq = txn.objectStore("outgoingRoomKeyRequests").openCursor(requestId); - cursorReq.onsuccess = () => { const cursor = cursorReq.result; - if (!cursor) { return; } - const data = cursor.value; - if (data.state != expectedState) { _logger.logger.warn(`Cannot delete room key request in state ${data.state} ` + `(expected ${expectedState})`); - return; } - cursor.delete(); }; - return promiseifyTxn(txn); - } // Olm Account + } + // Olm Account getAccount(txn, func) { const objectStore = txn.objectStore("account"); const getReq = objectStore.get("-"); - getReq.onsuccess = function () { try { func(getReq.result || null); @@ -359,16 +299,13 @@ } }; } - storeAccount(txn, accountPickle) { const objectStore = txn.objectStore("account"); objectStore.put(accountPickle, "-"); } - getCrossSigningKeys(txn, func) { const objectStore = txn.objectStore("account"); const getReq = objectStore.get("crossSigningKeys"); - getReq.onsuccess = function () { try { func(getReq.result || null); @@ -377,11 +314,9 @@ } }; } - getSecretStorePrivateKey(txn, func, type) { const objectStore = txn.objectStore("account"); const getReq = objectStore.get(`ssss_cache:${type}`); - getReq.onsuccess = function () { try { func(getReq.result || null); @@ -390,22 +325,20 @@ } }; } - storeCrossSigningKeys(txn, keys) { const objectStore = txn.objectStore("account"); objectStore.put(keys, "crossSigningKeys"); } - storeSecretStorePrivateKey(txn, type, key) { const objectStore = txn.objectStore("account"); objectStore.put(key, `ssss_cache:${type}`); - } // Olm Sessions + } + // Olm Sessions countEndToEndSessions(txn, func) { const objectStore = txn.objectStore("sessions"); const countReq = objectStore.count(); - countReq.onsuccess = function () { try { func(countReq.result); @@ -414,16 +347,13 @@ } }; } - getEndToEndSessions(deviceKey, txn, func) { const objectStore = txn.objectStore("sessions"); const idx = objectStore.index("deviceKey"); const getReq = idx.openCursor(deviceKey); const results = {}; - getReq.onsuccess = function () { const cursor = getReq.result; - if (cursor) { results[cursor.value.sessionId] = { session: cursor.value.session, @@ -439,11 +369,9 @@ } }; } - getEndToEndSession(deviceKey, sessionId, txn, func) { const objectStore = txn.objectStore("sessions"); const getReq = objectStore.get([deviceKey, sessionId]); - getReq.onsuccess = function () { try { if (getReq.result) { @@ -459,15 +387,12 @@ } }; } - getAllEndToEndSessions(txn, func) { const objectStore = txn.objectStore("sessions"); const getReq = objectStore.openCursor(); - getReq.onsuccess = function () { try { const cursor = getReq.result; - if (cursor) { func(cursor.value); cursor.continue(); @@ -479,7 +404,6 @@ } }; } - storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const objectStore = txn.objectStore("sessions"); objectStore.put({ @@ -489,7 +413,6 @@ lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs }); } - async storeEndToEndSessionProblem(deviceKey, type, fixed) { const txn = this.db.transaction("session_problems", "readwrite"); const objectStore = txn.objectStore("session_problems"); @@ -499,29 +422,24 @@ fixed, time: Date.now() }); - return promiseifyTxn(txn); + await promiseifyTxn(txn); } - async getEndToEndSessionProblem(deviceKey, timestamp) { - let result; + let result = null; const txn = this.db.transaction("session_problems", "readwrite"); const objectStore = txn.objectStore("session_problems"); const index = objectStore.index("deviceKey"); const req = index.getAll(deviceKey); - req.onsuccess = () => { const problems = req.result; - if (!problems.length) { result = null; return; } - problems.sort((a, b) => { return a.time - b.time; }); const lastProblem = problems[problems.length - 1]; - for (const problem of problems) { if (problem.time > timestamp) { result = Object.assign({}, problem, { @@ -530,19 +448,17 @@ return; } } - if (lastProblem.fixed) { result = null; } else { result = lastProblem; } }; - await promiseifyTxn(txn); return result; - } // FIXME: we should probably prune this when devices get deleted - + } + // FIXME: we should probably prune this when devices get deleted async filterOutNotifiedErrorDevices(devices) { const txn = this.db.transaction("notified_error_devices", "readwrite"); const objectStore = txn.objectStore("notified_error_devices"); @@ -554,7 +470,6 @@ deviceInfo } = device; const getReq = objectStore.get([userId, deviceInfo.deviceId]); - getReq.onsuccess = function () { if (!getReq.result) { objectStore.put({ @@ -563,21 +478,20 @@ }); ret.push(device); } - resolve(); }; }); })); return ret; - } // Inbound group sessions + } + // Inbound group sessions getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { let session = false; let withheld = false; const objectStore = txn.objectStore("inbound_group_sessions"); const getReq = objectStore.get([senderCurve25519Key, sessionId]); - getReq.onsuccess = function () { try { if (getReq.result) { @@ -585,7 +499,6 @@ } else { session = null; } - if (withheld !== false) { func(session, withheld); } @@ -593,10 +506,8 @@ abortWithException(txn, e); } }; - const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld"); const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]); - withheldGetReq.onsuccess = function () { try { if (withheldGetReq.result) { @@ -604,7 +515,6 @@ } else { withheld = null; } - if (session !== false) { func(session, withheld); } @@ -613,14 +523,11 @@ } }; } - getAllEndToEndInboundGroupSessions(txn, func) { const objectStore = txn.objectStore("inbound_group_sessions"); const getReq = objectStore.openCursor(); - getReq.onsuccess = function () { const cursor = getReq.result; - if (cursor) { try { func({ @@ -631,7 +538,6 @@ } catch (e) { abortWithException(txn, e); } - cursor.continue(); } else { try { @@ -642,7 +548,6 @@ } }; } - addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { const objectStore = txn.objectStore("inbound_group_sessions"); const addReq = objectStore.add({ @@ -650,21 +555,18 @@ sessionId, session: sessionData }); - addReq.onerror = ev => { - if (addReq.error.name === 'ConstraintError') { + if (addReq.error?.name === "ConstraintError") { // This stops the error from triggering the txn's onerror - ev.stopPropagation(); // ...and this stops it from aborting the transaction - + ev.stopPropagation(); + // ...and this stops it from aborting the transaction ev.preventDefault(); - _logger.logger.log("Ignoring duplicate inbound group session: " + senderCurve25519Key + " / " + sessionId); } else { abortWithException(txn, new Error("Failed to add inbound group session: " + addReq.error)); } }; } - storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { const objectStore = txn.objectStore("inbound_group_sessions"); objectStore.put({ @@ -673,7 +575,6 @@ session: sessionData }); } - storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) { const objectStore = txn.objectStore("inbound_group_sessions_withheld"); objectStore.put({ @@ -682,11 +583,9 @@ session: sessionData }); } - getEndToEndDeviceData(txn, func) { const objectStore = txn.objectStore("device_data"); const getReq = objectStore.get("-"); - getReq.onsuccess = function () { try { func(getReq.result || null); @@ -695,25 +594,20 @@ } }; } - storeEndToEndDeviceData(deviceData, txn) { const objectStore = txn.objectStore("device_data"); objectStore.put(deviceData, "-"); } - storeEndToEndRoom(roomId, roomInfo, txn) { const objectStore = txn.objectStore("rooms"); objectStore.put(roomInfo, roomId); } - getEndToEndRooms(txn, func) { const rooms = {}; const objectStore = txn.objectStore("rooms"); const getReq = objectStore.openCursor(); - getReq.onsuccess = function () { const cursor = getReq.result; - if (cursor) { rooms[cursor.key] = cursor.value; cursor.continue(); @@ -725,29 +619,25 @@ } } }; - } // session backups + } + // session backups getSessionsNeedingBackup(limit) { return new Promise((resolve, reject) => { const sessions = []; const txn = this.db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly"); txn.onerror = reject; - txn.oncomplete = function () { resolve(sessions); }; - const objectStore = txn.objectStore("sessions_needing_backup"); const sessionStore = txn.objectStore("inbound_group_sessions"); const getReq = objectStore.openCursor(); - getReq.onsuccess = function () { const cursor = getReq.result; - if (cursor) { const sessionGetReq = sessionStore.get(cursor.key); - sessionGetReq.onsuccess = function () { sessions.push({ senderKey: sessionGetReq.result.senderCurve25519Key, @@ -755,7 +645,6 @@ sessionData: sessionGetReq.result.session }); }; - if (!limit || sessions.length < limit) { cursor.continue(); } @@ -763,26 +652,21 @@ }; }); } - countSessionsNeedingBackup(txn) { if (!txn) { txn = this.db.transaction("sessions_needing_backup", "readonly"); } - const objectStore = txn.objectStore("sessions_needing_backup"); return new Promise((resolve, reject) => { const req = objectStore.count(); req.onerror = reject; - req.onsuccess = () => resolve(req.result); }); } - async unmarkSessionsNeedingBackup(sessions, txn) { if (!txn) { txn = this.db.transaction("sessions_needing_backup", "readwrite"); } - const objectStore = txn.objectStore("sessions_needing_backup"); await Promise.all(sessions.map(session => { return new Promise((resolve, reject) => { @@ -792,12 +676,10 @@ }); })); } - async markSessionsNeedingBackup(sessions, txn) { if (!txn) { txn = this.db.transaction("sessions_needing_backup", "readwrite"); } - const objectStore = txn.objectStore("sessions_needing_backup"); await Promise.all(sessions.map(session => { return new Promise((resolve, reject) => { @@ -810,15 +692,12 @@ }); })); } - addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) { if (!txn) { txn = this.db.transaction("shared_history_inbound_group_sessions", "readwrite"); } - const objectStore = txn.objectStore("shared_history_inbound_group_sessions"); const req = objectStore.get([roomId]); - req.onsuccess = () => { const { sessions @@ -832,12 +711,10 @@ }); }; } - getSharedHistoryInboundGroupSessions(roomId, txn) { if (!txn) { txn = this.db.transaction("shared_history_inbound_group_sessions", "readonly"); } - const objectStore = txn.objectStore("shared_history_inbound_group_sessions"); const req = objectStore.get([roomId]); return new Promise((resolve, reject) => { @@ -849,19 +726,15 @@ }; resolve(sessions); }; - req.onerror = reject; }); } - addParkedSharedHistory(roomId, parkedData, txn) { if (!txn) { txn = this.db.transaction("parked_shared_history", "readwrite"); } - const objectStore = txn.objectStore("parked_shared_history"); const req = objectStore.get([roomId]); - req.onsuccess = () => { const { parked @@ -875,45 +748,37 @@ }); }; } - takeParkedSharedHistory(roomId, txn) { if (!txn) { txn = this.db.transaction("parked_shared_history", "readwrite"); } - const cursorReq = txn.objectStore("parked_shared_history").openCursor(roomId); return new Promise((resolve, reject) => { cursorReq.onsuccess = () => { const cursor = cursorReq.result; - if (!cursor) { resolve([]); + return; } - const data = cursor.value; cursor.delete(); resolve(data); }; - cursorReq.onerror = reject; }); } - doTxn(mode, stores, func, log = _logger.logger) { let startTime; let description; - if (PROFILE_TRANSACTIONS) { const txnId = this.nextTxnId++; startTime = Date.now(); description = `${mode} crypto store transaction ${txnId} in ${stores}`; log.debug(`Starting ${description}`); } - const txn = this.db.transaction(stores, mode); const promise = promiseifyTxn(txn); const result = func(txn); - if (PROFILE_TRANSACTIONS) { promise.then(() => { const elapsedTime = Date.now() - startTime; @@ -923,95 +788,75 @@ log.error(`Failed ${description}, took ${elapsedTime} ms`); }); } - return promise.then(() => { return result; }); } - } - exports.Backend = Backend; +const DB_MIGRATIONS = [db => { + createDatabase(db); +}, db => { + db.createObjectStore("account"); +}, db => { + const sessionsStore = db.createObjectStore("sessions", { + keyPath: ["deviceKey", "sessionId"] + }); + sessionsStore.createIndex("deviceKey", "deviceKey"); +}, db => { + db.createObjectStore("inbound_group_sessions", { + keyPath: ["senderCurve25519Key", "sessionId"] + }); +}, db => { + db.createObjectStore("device_data"); +}, db => { + db.createObjectStore("rooms"); +}, db => { + db.createObjectStore("sessions_needing_backup", { + keyPath: ["senderCurve25519Key", "sessionId"] + }); +}, db => { + db.createObjectStore("inbound_group_sessions_withheld", { + keyPath: ["senderCurve25519Key", "sessionId"] + }); +}, db => { + const problemsStore = db.createObjectStore("session_problems", { + keyPath: ["deviceKey", "time"] + }); + problemsStore.createIndex("deviceKey", "deviceKey"); + db.createObjectStore("notified_error_devices", { + keyPath: ["userId", "deviceId"] + }); +}, db => { + db.createObjectStore("shared_history_inbound_group_sessions", { + keyPath: ["roomId"] + }); +}, db => { + db.createObjectStore("parked_shared_history", { + keyPath: ["roomId"] + }); +} +// Expand as needed. +]; +const VERSION = DB_MIGRATIONS.length; +exports.VERSION = VERSION; function upgradeDatabase(db, oldVersion) { _logger.logger.log(`Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`); - - if (oldVersion < 1) { - // The database did not previously exist. - createDatabase(db); - } - - if (oldVersion < 2) { - db.createObjectStore("account"); - } - - if (oldVersion < 3) { - const sessionsStore = db.createObjectStore("sessions", { - keyPath: ["deviceKey", "sessionId"] - }); - sessionsStore.createIndex("deviceKey", "deviceKey"); - } - - if (oldVersion < 4) { - db.createObjectStore("inbound_group_sessions", { - keyPath: ["senderCurve25519Key", "sessionId"] - }); - } - - if (oldVersion < 5) { - db.createObjectStore("device_data"); - } - - if (oldVersion < 6) { - db.createObjectStore("rooms"); - } - - if (oldVersion < 7) { - db.createObjectStore("sessions_needing_backup", { - keyPath: ["senderCurve25519Key", "sessionId"] - }); - } - - if (oldVersion < 8) { - db.createObjectStore("inbound_group_sessions_withheld", { - keyPath: ["senderCurve25519Key", "sessionId"] - }); - } - - if (oldVersion < 9) { - const problemsStore = db.createObjectStore("session_problems", { - keyPath: ["deviceKey", "time"] - }); - problemsStore.createIndex("deviceKey", "deviceKey"); - db.createObjectStore("notified_error_devices", { - keyPath: ["userId", "deviceId"] - }); - } - - if (oldVersion < 10) { - db.createObjectStore("shared_history_inbound_group_sessions", { - keyPath: ["roomId"] - }); - } - - if (oldVersion < 11) { - db.createObjectStore("parked_shared_history", { - keyPath: ["roomId"] - }); - } // Expand as needed. - + DB_MIGRATIONS.forEach((migration, index) => { + if (oldVersion <= index) migration(db); + }); } - function createDatabase(db) { const outgoingRoomKeyRequestsStore = db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" - }); // we assume that the RoomKeyRequestBody will have room_id and session_id - // properties, to make the index efficient. + }); + // we assume that the RoomKeyRequestBody will have room_id and session_id + // properties, to make the index efficient. outgoingRoomKeyRequestsStore.createIndex("session", ["requestBody.room_id", "requestBody.session_id"]); outgoingRoomKeyRequestsStore.createIndex("state", "state"); } - /* * Aborts a transaction with a given exception * The transaction promise will be rejected with this exception. @@ -1021,40 +866,34 @@ // We could alternatively make the thing we pass back to the app // an object containing the transaction and exception. txn._mx_abortexception = e; - try { txn.abort(); - } catch (e) {// sometimes we won't be able to abort the transaction + } catch (e) { + // sometimes we won't be able to abort the transaction // (ie. if it's aborted or completed) } } - function promiseifyTxn(txn) { return new Promise((resolve, reject) => { txn.oncomplete = () => { if (txn._mx_abortexception !== undefined) { reject(txn._mx_abortexception); } - resolve(null); }; - txn.onerror = event => { if (txn._mx_abortexception !== undefined) { reject(txn._mx_abortexception); } else { _logger.logger.log("Error performing indexeddb txn", event); - reject(txn.error); } }; - txn.onabort = event => { if (txn._mx_abortexception !== undefined) { reject(txn._mx_abortexception); } else { _logger.logger.log("Error performing indexeddb txn", event); - reject(txn.error); } }; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/indexeddb-crypto-store.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,128 +4,98 @@ value: true }); exports.IndexedDBCryptoStore = void 0; - var _logger = require("../../logger"); - var _localStorageCryptoStore = require("./localStorage-crypto-store"); - var _memoryCryptoStore = require("./memory-crypto-store"); - var IndexedDBCryptoStoreBackend = _interopRequireWildcard(require("./indexeddb-crypto-store-backend")); - var _errors = require("../../errors"); - var IndexedDBHelpers = _interopRequireWildcard(require("../../indexeddb-helpers")); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * Internal module. indexeddb storage for e2e. - * - * @module */ /** * An implementation of CryptoStore, which is normally backed by an indexeddb, * but with fallback to MemoryCryptoStore. - * - * @implements {module:crypto/store/base~CryptoStore} */ class IndexedDBCryptoStore { static exists(indexedDB, dbName) { return IndexedDBHelpers.exists(indexedDB, dbName); } - /** * Create a new IndexedDBCryptoStore * - * @param {IDBFactory} indexedDB global indexedDB instance - * @param {string} dbName name of db to connect to + * @param indexedDB - global indexedDB instance + * @param dbName - name of db to connect to */ constructor(indexedDB, dbName) { this.indexedDB = indexedDB; this.dbName = dbName; - - _defineProperty(this, "backendPromise", null); - - _defineProperty(this, "backend", null); + _defineProperty(this, "backendPromise", void 0); + _defineProperty(this, "backend", void 0); } + /** * Ensure the database exists and is up-to-date, or fall back to * a local storage or in-memory store. * * This must be called before the store can be used. * - * @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend, + * @returns resolves to either an IndexedDBCryptoStoreBackend.Backend, * or a MemoryCryptoStore */ - - startup() { if (this.backendPromise) { return this.backendPromise; } - this.backendPromise = new Promise((resolve, reject) => { if (!this.indexedDB) { - reject(new Error('no indexeddb support available')); + reject(new Error("no indexeddb support available")); return; } - _logger.logger.log(`connecting to indexeddb ${this.dbName}`); - const req = this.indexedDB.open(this.dbName, IndexedDBCryptoStoreBackend.VERSION); - req.onupgradeneeded = ev => { const db = req.result; const oldVersion = ev.oldVersion; IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion); }; - req.onblocked = () => { _logger.logger.log(`can't yet open IndexedDBCryptoStore because it is open elsewhere`); }; - req.onerror = ev => { _logger.logger.log("Error connecting to indexeddb", ev); - reject(req.error); }; - req.onsuccess = () => { const db = req.result; - _logger.logger.log(`connected to indexeddb ${this.dbName}`); - resolve(new IndexedDBCryptoStoreBackend.Backend(db)); }; }).then(backend => { // Edge has IndexedDB but doesn't support compund keys which we use fairly extensively. // Try a dummy query which will fail if the browser doesn't support compund keys, so // we can fall back to a different backend. - return backend.doTxn('readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { - backend.getEndToEndInboundGroupSession('', '', txn, () => {}); + return backend.doTxn("readonly", [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD], txn => { + backend.getEndToEndInboundGroupSession("", "", txn, () => {}); }).then(() => backend); }).catch(e => { - if (e.name === 'VersionError') { - _logger.logger.warn("Crypto DB is too new for us to use!", e); // don't fall back to a different store: the user has crypto data + if (e.name === "VersionError") { + _logger.logger.warn("Crypto DB is too new for us to use!", e); + // don't fall back to a different store: the user has crypto data // in this db so we should use it or nothing at all. - - - throw new _errors.InvalidCryptoStoreError(_errors.InvalidCryptoStoreError.TOO_NEW); + throw new _errors.InvalidCryptoStoreError(_errors.InvalidCryptoStoreState.TooNew); } - _logger.logger.warn(`unable to connect to indexeddb ${this.dbName}` + `: falling back to localStorage store: ${e}`); - try { return new _localStorageCryptoStore.LocalStorageCryptoStore(global.localStorage); } catch (e) { _logger.logger.warn(`unable to open localStorage: falling back to in-memory store: ${e}`); - return new _memoryCryptoStore.MemoryCryptoStore(); } }).then(backend => { @@ -134,37 +104,29 @@ }); return this.backendPromise; } + /** * Delete all data from this store. * - * @returns {Promise} resolves when the store has been cleared. + * @returns resolves when the store has been cleared. */ - - deleteAllData() { return new Promise((resolve, reject) => { if (!this.indexedDB) { - reject(new Error('no indexeddb support available')); + reject(new Error("no indexeddb support available")); return; } - _logger.logger.log(`Removing indexeddb instance: ${this.dbName}`); - const req = this.indexedDB.deleteDatabase(this.dbName); - req.onblocked = () => { _logger.logger.log(`can't yet delete IndexedDBCryptoStore because it is open elsewhere`); }; - req.onerror = ev => { _logger.logger.log("Error deleting data from indexeddb", ev); - reject(req.error); }; - req.onsuccess = () => { _logger.logger.log(`Removed indexeddb instance: ${this.dbName}`); - resolve(); }; }).catch(e => { @@ -174,319 +136,295 @@ _logger.logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`); }); } + /** * Look for an existing outgoing room key request, and if none is found, * add a new one * - * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * @returns resolves to + * {@link OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ - - getOrAddOutgoingRoomKeyRequest(request) { return this.backend.getOrAddOutgoingRoomKeyRequest(request); } + /** * Look for an existing room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {Promise} resolves to the matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the matching + * {@link OutgoingRoomKeyRequest}, or null if * not found */ - - getOutgoingRoomKeyRequest(requestBody) { return this.backend.getOutgoingRoomKeyRequest(requestBody); } + /** * Look for room key requests by state * - * @param {Array} wantedStates list of acceptable states + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to the a - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the a + * {@link OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states. If there are multiple * requests in those states, an arbitrary one is chosen. */ - - getOutgoingRoomKeyRequestByState(wantedStates) { return this.backend.getOutgoingRoomKeyRequestByState(wantedStates); } + /** * Look for room key requests by state – * unlike above, return a list of all entries in one state. * - * @param {Number} wantedState - * @return {Promise>} Returns an array of requests in the given state + * @returns Returns an array of requests in the given state */ - - getAllOutgoingRoomKeyRequestsByState(wantedState) { return this.backend.getAllOutgoingRoomKeyRequestsByState(wantedState); } + /** * Look for room key requests by target device and state * - * @param {string} userId Target user ID - * @param {string} deviceId Target device ID - * @param {Array} wantedStates list of acceptable states + * @param userId - Target user ID + * @param deviceId - Target device ID + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to a list of all the - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to a list of all the + * {@link OutgoingRoomKeyRequest} */ - - getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { return this.backend.getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates); } + /** * Look for an existing room key request by id and state, and update it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in - * @param {Object} updates name/value map of updates to apply + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in + * @param updates - name/value map of updates to apply * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to + * {@link OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ - - updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { return this.backend.updateOutgoingRoomKeyRequest(requestId, expectedState, updates); } + /** * Look for an existing room key request by id and state, and delete it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in * - * @returns {Promise} resolves once the operation is completed + * @returns resolves once the operation is completed */ - - deleteOutgoingRoomKeyRequest(requestId, expectedState) { return this.backend.deleteOutgoingRoomKeyRequest(requestId, expectedState); - } // Olm Account + } + + // Olm Account /* * Get the account pickle from the store. * This requires an active transaction. See doTxn(). * - * @param {*} txn An active transaction. See doTxn(). - * @param {function(string)} func Called with the account pickle + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the account pickle */ - - getAccount(txn, func) { this.backend.getAccount(txn, func); } + /** * Write the account pickle to the store. * This requires an active transaction. See doTxn(). * - * @param {*} txn An active transaction. See doTxn(). - * @param {string} accountPickle The new account pickle to store. + * @param txn - An active transaction. See doTxn(). + * @param accountPickle - The new account pickle to store. */ - - storeAccount(txn, accountPickle) { this.backend.storeAccount(txn, accountPickle); } + /** * Get the public part of the cross-signing keys (eg. self-signing key, * user signing key). * - * @param {*} txn An active transaction. See doTxn(). - * @param {function(string)} func Called with the account keys object: - * { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the account keys object: + * `{ key_type: base64 encoded seed }` where key type = user_signing_key_seed or self_signing_key_seed */ - - getCrossSigningKeys(txn, func) { this.backend.getCrossSigningKeys(txn, func); } + /** - * @param {*} txn An active transaction. See doTxn(). - * @param {function(string)} func Called with the private key - * @param {string} type A key type + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the private key + * @param type - A key type */ - - getSecretStorePrivateKey(txn, func, type) { this.backend.getSecretStorePrivateKey(txn, func, type); } + /** * Write the cross-signing keys back to the store * - * @param {*} txn An active transaction. See doTxn(). - * @param {string} keys keys object as getCrossSigningKeys() + * @param txn - An active transaction. See doTxn(). + * @param keys - keys object as getCrossSigningKeys() */ - - storeCrossSigningKeys(txn, keys) { this.backend.storeCrossSigningKeys(txn, keys); } + /** * Write the cross-signing private keys back to the store * - * @param {*} txn An active transaction. See doTxn(). - * @param {string} type The type of cross-signing private key to store - * @param {string} key keys object as getCrossSigningKeys() + * @param txn - An active transaction. See doTxn(). + * @param type - The type of cross-signing private key to store + * @param key - keys object as getCrossSigningKeys() */ - - storeSecretStorePrivateKey(txn, type, key) { this.backend.storeSecretStorePrivateKey(txn, type, key); - } // Olm sessions + } + + // Olm sessions /** * Returns the number of end-to-end sessions in the store - * @param {*} txn An active transaction. See doTxn(). - * @param {function(int)} func Called with the count of sessions + * @param txn - An active transaction. See doTxn(). + * @param func - Called with the count of sessions */ - - countEndToEndSessions(txn, func) { this.backend.countEndToEndSessions(txn, func); } + /** * Retrieve a specific end-to-end session between the logged-in user * and another device. - * @param {string} deviceKey The public key of the other device. - * @param {string} sessionId The ID of the session to retrieve - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called with A map from sessionId + * @param deviceKey - The public key of the other device. + * @param sessionId - The ID of the session to retrieve + * @param txn - An active transaction. See doTxn(). + * @param func - Called with A map from sessionId * to session information object with 'session' key being the * Base64 end-to-end session and lastReceivedMessageTs being the * timestamp in milliseconds at which the session last received * a message. */ - - getEndToEndSession(deviceKey, sessionId, txn, func) { this.backend.getEndToEndSession(deviceKey, sessionId, txn, func); } + /** * Retrieve the end-to-end sessions between the logged-in user and another * device. - * @param {string} deviceKey The public key of the other device. - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called with A map from sessionId + * @param deviceKey - The public key of the other device. + * @param txn - An active transaction. See doTxn(). + * @param func - Called with A map from sessionId * to session information object with 'session' key being the * Base64 end-to-end session and lastReceivedMessageTs being the * timestamp in milliseconds at which the session last received * a message. */ - - getEndToEndSessions(deviceKey, txn, func) { this.backend.getEndToEndSessions(deviceKey, txn, func); } + /** * Retrieve all end-to-end sessions - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called one for each session with + * @param txn - An active transaction. See doTxn(). + * @param func - Called one for each session with * an object with, deviceKey, lastReceivedMessageTs, sessionId * and session keys. */ - - getAllEndToEndSessions(txn, func) { this.backend.getAllEndToEndSessions(txn, func); } + /** * Store a session between the logged-in user and another device - * @param {string} deviceKey The public key of the other device. - * @param {string} sessionId The ID for this end-to-end session. - * @param {string} sessionInfo Session information object - * @param {*} txn An active transaction. See doTxn(). + * @param deviceKey - The public key of the other device. + * @param sessionId - The ID for this end-to-end session. + * @param sessionInfo - Session information object + * @param txn - An active transaction. See doTxn(). */ - - storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { this.backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn); } - storeEndToEndSessionProblem(deviceKey, type, fixed) { return this.backend.storeEndToEndSessionProblem(deviceKey, type, fixed); } - getEndToEndSessionProblem(deviceKey, timestamp) { return this.backend.getEndToEndSessionProblem(deviceKey, timestamp); } - filterOutNotifiedErrorDevices(devices) { return this.backend.filterOutNotifiedErrorDevices(devices); - } // Inbound group sessions + } + + // Inbound group sessions /** * Retrieve the end-to-end inbound group session for a given * server key and session ID - * @param {string} senderCurve25519Key The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called with A map from sessionId + * @param senderCurve25519Key - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param txn - An active transaction. See doTxn(). + * @param func - Called with A map from sessionId * to Base64 end-to-end session. */ - - getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { this.backend.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func); } + /** * Fetches all inbound group sessions in the store - * @param {*} txn An active transaction. See doTxn(). - * @param {function(object)} func Called once for each group session - * in the store with an object having keys {senderKey, sessionId, - * sessionData}, then once with null to indicate the end of the list. + * @param txn - An active transaction. See doTxn(). + * @param func - Called once for each group session + * in the store with an object having keys `{senderKey, sessionId, sessionData}`, + * then once with null to indicate the end of the list. */ - - getAllEndToEndInboundGroupSessions(txn, func) { this.backend.getAllEndToEndInboundGroupSessions(txn, func); } + /** * Adds an end-to-end inbound group session to the store. * If there already exists an inbound group session with the same * senderCurve25519Key and sessionID, the session will not be added. - * @param {string} senderCurve25519Key The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {object} sessionData The session data structure - * @param {*} txn An active transaction. See doTxn(). + * @param senderCurve25519Key - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param sessionData - The session data structure + * @param txn - An active transaction. See doTxn(). */ - - addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { this.backend.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); } + /** * Writes an end-to-end inbound group session to the store. * If there already exists an inbound group session with the same * senderCurve25519Key and sessionID, it will be overwritten. - * @param {string} senderCurve25519Key The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {object} sessionData The session data structure - * @param {*} txn An active transaction. See doTxn(). + * @param senderCurve25519Key - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param sessionData - The session data structure + * @param txn - An active transaction. See doTxn(). */ - - storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { this.backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); } - storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) { this.backend.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn); - } // End-to-end device tracking + } + + // End-to-end device tracking /** * Store the state of all tracked devices @@ -495,177 +433,153 @@ * These all need to be written out in full each time such that the snapshot * is always consistent, so they are stored in one object. * - * @param {Object} deviceData - * @param {*} txn An active transaction. See doTxn(). + * @param txn - An active transaction. See doTxn(). */ - - storeEndToEndDeviceData(deviceData, txn) { this.backend.storeEndToEndDeviceData(deviceData, txn); } + /** * Get the state of all tracked devices * - * @param {*} txn An active transaction. See doTxn(). - * @param {function(Object)} func Function called with the + * @param txn - An active transaction. See doTxn(). + * @param func - Function called with the * device data */ - - getEndToEndDeviceData(txn, func) { this.backend.getEndToEndDeviceData(txn, func); - } // End to End Rooms + } + + // End to End Rooms /** * Store the end-to-end state for a room. - * @param {string} roomId The room's ID. - * @param {object} roomInfo The end-to-end info for the room. - * @param {*} txn An active transaction. See doTxn(). + * @param roomId - The room's ID. + * @param roomInfo - The end-to-end info for the room. + * @param txn - An active transaction. See doTxn(). */ - - storeEndToEndRoom(roomId, roomInfo, txn) { this.backend.storeEndToEndRoom(roomId, roomInfo, txn); } + /** - * Get an object of roomId->roomInfo for all e2e rooms in the store - * @param {*} txn An active transaction. See doTxn(). - * @param {function(Object)} func Function called with the end to end encrypted rooms + * Get an object of `roomId->roomInfo` for all e2e rooms in the store + * @param txn - An active transaction. See doTxn(). + * @param func - Function called with the end-to-end encrypted rooms */ - - getEndToEndRooms(txn, func) { this.backend.getEndToEndRooms(txn, func); - } // session backups + } + + // session backups /** * Get the inbound group sessions that need to be backed up. - * @param {number} limit The maximum number of sessions to retrieve. 0 + * @param limit - The maximum number of sessions to retrieve. 0 * for no limit. - * @returns {Promise} resolves to an array of inbound group sessions + * @returns resolves to an array of inbound group sessions */ - - getSessionsNeedingBackup(limit) { return this.backend.getSessionsNeedingBackup(limit); } + /** * Count the inbound group sessions that need to be backed up. - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} resolves to the number of sessions + * @param txn - An active transaction. See doTxn(). (optional) + * @returns resolves to the number of sessions */ - - countSessionsNeedingBackup(txn) { return this.backend.countSessionsNeedingBackup(txn); } + /** * Unmark sessions as needing to be backed up. - * @param {Array} sessions The sessions that need to be backed up. - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} resolves when the sessions are unmarked + * @param sessions - The sessions that need to be backed up. + * @param txn - An active transaction. See doTxn(). (optional) + * @returns resolves when the sessions are unmarked */ - - unmarkSessionsNeedingBackup(sessions, txn) { return this.backend.unmarkSessionsNeedingBackup(sessions, txn); } + /** * Mark sessions as needing to be backed up. - * @param {Array} sessions The sessions that need to be backed up. - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} resolves when the sessions are marked + * @param sessions - The sessions that need to be backed up. + * @param txn - An active transaction. See doTxn(). (optional) + * @returns resolves when the sessions are marked */ - - markSessionsNeedingBackup(sessions, txn) { return this.backend.markSessionsNeedingBackup(sessions, txn); } + /** * Add a shared-history group session for a room. - * @param {string} roomId The room that the key belongs to - * @param {string} senderKey The sender's curve 25519 key - * @param {string} sessionId The ID of the session - * @param {*} txn An active transaction. See doTxn(). (optional) + * @param roomId - The room that the key belongs to + * @param senderKey - The sender's curve 25519 key + * @param sessionId - The ID of the session + * @param txn - An active transaction. See doTxn(). (optional) */ - - addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) { this.backend.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn); } + /** * Get the shared-history group session for a room. - * @param {string} roomId The room that the key belongs to - * @param {*} txn An active transaction. See doTxn(). (optional) - * @returns {Promise} Resolves to an array of [senderKey, sessionId] + * @param roomId - The room that the key belongs to + * @param txn - An active transaction. See doTxn(). (optional) + * @returns Promise which resolves to an array of [senderKey, sessionId] */ - - getSharedHistoryInboundGroupSessions(roomId, txn) { return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn); } + /** * Park a shared-history group session for a room we may be invited to later. */ - - addParkedSharedHistory(roomId, parkedData, txn) { this.backend.addParkedSharedHistory(roomId, parkedData, txn); } + /** * Pop out all shared-history group sessions for a room. */ - - takeParkedSharedHistory(roomId, txn) { return this.backend.takeParkedSharedHistory(roomId, txn); } + /** * Perform a transaction on the crypto store. Any store methods * that require a transaction (txn) object to be passed in may * only be called within a callback of either this function or * one of the store functions operating on the same transaction. * - * @param {string} mode 'readwrite' if you need to call setter + * @param mode - 'readwrite' if you need to call setter * functions with this transaction. Otherwise, 'readonly'. - * @param {string[]} stores List IndexedDBCryptoStore.STORE_* + * @param stores - List IndexedDBCryptoStore.STORE_* * options representing all types of object that will be * accessed or written to with this transaction. - * @param {function(*)} func Function called with the + * @param func - Function called with the * transaction object: an opaque object that should be passed * to store functions. - * @param {Logger} [log] A possibly customised log - * @return {Promise} Promise that resolves with the result of the `func` + * @param log - A possibly customised log + * @returns Promise that resolves with the result of the `func` * when the transaction is complete. If the backend is * async (ie. the indexeddb backend) any of the callback * functions throwing an exception will cause this promise to * reject with that exception. On synchronous backends, the * exception will propagate to the caller of the getFoo method. */ - - doTxn(mode, stores, func, log) { return this.backend.doTxn(mode, stores, func, log); } - } - exports.IndexedDBCryptoStore = IndexedDBCryptoStore; - -_defineProperty(IndexedDBCryptoStore, "STORE_ACCOUNT", 'account'); - -_defineProperty(IndexedDBCryptoStore, "STORE_SESSIONS", 'sessions'); - -_defineProperty(IndexedDBCryptoStore, "STORE_INBOUND_GROUP_SESSIONS", 'inbound_group_sessions'); - -_defineProperty(IndexedDBCryptoStore, "STORE_INBOUND_GROUP_SESSIONS_WITHHELD", 'inbound_group_sessions_withheld'); - -_defineProperty(IndexedDBCryptoStore, "STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS", 'shared_history_inbound_group_sessions'); - -_defineProperty(IndexedDBCryptoStore, "STORE_PARKED_SHARED_HISTORY", 'parked_shared_history'); - -_defineProperty(IndexedDBCryptoStore, "STORE_DEVICE_DATA", 'device_data'); - -_defineProperty(IndexedDBCryptoStore, "STORE_ROOMS", 'rooms'); - -_defineProperty(IndexedDBCryptoStore, "STORE_BACKUP", 'sessions_needing_backup'); \ No newline at end of file +_defineProperty(IndexedDBCryptoStore, "STORE_ACCOUNT", "account"); +_defineProperty(IndexedDBCryptoStore, "STORE_SESSIONS", "sessions"); +_defineProperty(IndexedDBCryptoStore, "STORE_INBOUND_GROUP_SESSIONS", "inbound_group_sessions"); +_defineProperty(IndexedDBCryptoStore, "STORE_INBOUND_GROUP_SESSIONS_WITHHELD", "inbound_group_sessions_withheld"); +_defineProperty(IndexedDBCryptoStore, "STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS", "shared_history_inbound_group_sessions"); +_defineProperty(IndexedDBCryptoStore, "STORE_PARKED_SHARED_HISTORY", "parked_shared_history"); +_defineProperty(IndexedDBCryptoStore, "STORE_DEVICE_DATA", "device_data"); +_defineProperty(IndexedDBCryptoStore, "STORE_ROOMS", "rooms"); +_defineProperty(IndexedDBCryptoStore, "STORE_BACKUP", "sessions_needing_backup"); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/localStorage-crypto-store.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/localStorage-crypto-store.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/localStorage-crypto-store.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/localStorage-crypto-store.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,11 +4,9 @@ value: true }); exports.LocalStorageCryptoStore = void 0; - var _logger = require("../../logger"); - var _memoryCryptoStore = require("./memory-crypto-store"); - +var _utils = require("../../utils"); /* Copyright 2017 - 2021 The Matrix.org Foundation C.I.C. @@ -31,9 +29,8 @@ * some things backed by localStorage. It exists because indexedDB * is broken in Firefox private mode or set to, "will not remember * history". - * - * @module */ + const E2E_PREFIX = "crypto."; const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys"; @@ -43,67 +40,54 @@ const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/"; const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/"; const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup"; - function keyEndToEndSessions(deviceKey) { return E2E_PREFIX + "sessions/" + deviceKey; } - function keyEndToEndSessionProblems(deviceKey) { return E2E_PREFIX + "session.problems/" + deviceKey; } - function keyEndToEndInboundGroupSession(senderKey, sessionId) { return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId; } - function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) { return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId; } - function keyEndToEndRoomsPrefix(roomId) { return KEY_ROOMS_PREFIX + roomId; } -/** - * @implements {module:crypto/store/base~CryptoStore} - */ - - class LocalStorageCryptoStore extends _memoryCryptoStore.MemoryCryptoStore { static exists(store) { const length = store.length; - for (let i = 0; i < length; i++) { - if (store.key(i).startsWith(E2E_PREFIX)) { + if (store.key(i)?.startsWith(E2E_PREFIX)) { return true; } } - return false; } - constructor(store) { super(); this.store = store; - } // Olm Sessions + } + // Olm Sessions countEndToEndSessions(txn, func) { let count = 0; - for (let i = 0; i < this.store.length; ++i) { - if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count; + if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) ++count; } - func(count); - } // eslint-disable-next-line @typescript-eslint/naming-convention - + } + // eslint-disable-next-line @typescript-eslint/naming-convention _getEndToEndSessions(deviceKey) { const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey)); - const fixedSessions = {}; // fix up any old sessions to be objects rather than just the base64 pickle + const fixedSessions = {}; + // fix up any old sessions to be objects rather than just the base64 pickle for (const [sid, val] of Object.entries(sessions || {})) { - if (typeof val === 'string') { + if (typeof val === "string") { fixedSessions[sid] = { session: val }; @@ -111,38 +95,30 @@ fixedSessions[sid] = val; } } - return fixedSessions; } - getEndToEndSession(deviceKey, sessionId, txn, func) { const sessions = this._getEndToEndSessions(deviceKey); - func(sessions[sessionId] || {}); } - getEndToEndSessions(deviceKey, txn, func) { func(this._getEndToEndSessions(deviceKey) || {}); } - getAllEndToEndSessions(txn, func) { for (let i = 0; i < this.store.length; ++i) { - if (this.store.key(i).startsWith(keyEndToEndSessions(''))) { - const deviceKey = this.store.key(i).split('/')[1]; - + if (this.store.key(i)?.startsWith(keyEndToEndSessions(""))) { + const deviceKey = this.store.key(i).split("/")[1]; for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) { func(sess); } } } } - storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const sessions = this._getEndToEndSessions(deviceKey) || {}; sessions[sessionId] = sessionInfo; setJsonItem(this.store, keyEndToEndSessions(deviceKey), sessions); } - async storeEndToEndSessionProblem(deviceKey, type, fixed) { const key = keyEndToEndSessionProblems(deviceKey); const problems = getJsonItem(this.store, key) || []; @@ -156,17 +132,13 @@ }); setJsonItem(this.store, key, problems); } - async getEndToEndSessionProblem(deviceKey, timestamp) { const key = keyEndToEndSessionProblems(deviceKey); const problems = getJsonItem(this.store, key) || []; - if (!problems.length) { return null; } - const lastProblem = problems[problems.length - 1]; - for (const problem of problems) { if (problem.time > timestamp) { return Object.assign({}, problem, { @@ -174,55 +146,50 @@ }); } } - if (lastProblem.fixed) { return null; } else { return lastProblem; } } - async filterOutNotifiedErrorDevices(devices) { const notifiedErrorDevices = getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {}; const ret = []; - for (const device of devices) { const { userId, deviceInfo } = device; - if (userId in notifiedErrorDevices) { if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) { ret.push(device); - notifiedErrorDevices[userId][deviceInfo.deviceId] = true; + (0, _utils.safeSet)(notifiedErrorDevices[userId], deviceInfo.deviceId, true); } } else { ret.push(device); - notifiedErrorDevices[userId] = { + (0, _utils.safeSet)(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true - }; + }); } } - setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices); return ret; - } // Inbound Group Sessions + } + // Inbound Group Sessions getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { func(getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId)), getJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId))); } - getAllEndToEndInboundGroupSessions(txn, func) { for (let i = 0; i < this.store.length; ++i) { const key = this.store.key(i); - - if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) { + if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) { // we can't use split, as the components we are trying to split out // might themselves contain '/' characters. We rely on the // senderKey being a (32-byte) curve25519 key, base64-encoded // (hence 43 characters long). + func({ senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43), sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44), @@ -230,58 +197,44 @@ }); } } - func(null); } - addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { const existing = getJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId)); - if (!existing) { this.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); } } - storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { setJsonItem(this.store, keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId), sessionData); } - storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) { setJsonItem(this.store, keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId), sessionData); } - getEndToEndDeviceData(txn, func) { func(getJsonItem(this.store, KEY_DEVICE_DATA)); } - storeEndToEndDeviceData(deviceData, txn) { setJsonItem(this.store, KEY_DEVICE_DATA, deviceData); } - storeEndToEndRoom(roomId, roomInfo, txn) { setJsonItem(this.store, keyEndToEndRoomsPrefix(roomId), roomInfo); } - getEndToEndRooms(txn, func) { const result = {}; - const prefix = keyEndToEndRoomsPrefix(''); - + const prefix = keyEndToEndRoomsPrefix(""); for (let i = 0; i < this.store.length; ++i) { const key = this.store.key(i); - - if (key.startsWith(prefix)) { + if (key?.startsWith(prefix)) { const roomId = key.slice(prefix.length); result[roomId] = getJsonItem(this.store, key); } } - func(result); } - getSessionsNeedingBackup(limit) { const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; const sessions = []; - for (const session in sessionsNeedingBackup) { if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) { // see getAllEndToEndInboundGroupSessions for the magic number explanations @@ -294,104 +247,83 @@ sessionData: sessionData }); }); - if (limit && sessions.length >= limit) { break; } } } - return Promise.resolve(sessions); } - countSessionsNeedingBackup() { const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; return Promise.resolve(Object.keys(sessionsNeedingBackup).length); } - unmarkSessionsNeedingBackup(sessions) { const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; - for (const session of sessions) { - delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; + delete sessionsNeedingBackup[session.senderKey + "/" + session.sessionId]; } - setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup); return Promise.resolve(); } - markSessionsNeedingBackup(sessions) { const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; - for (const session of sessions) { - sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; + sessionsNeedingBackup[session.senderKey + "/" + session.sessionId] = true; } - setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup); return Promise.resolve(); } + /** * Delete all data from this store. * - * @returns {Promise} Promise which resolves when the store has been cleared. + * @returns Promise which resolves when the store has been cleared. */ - - deleteAllData() { this.store.removeItem(KEY_END_TO_END_ACCOUNT); return Promise.resolve(); - } // Olm account + } + // Olm account getAccount(txn, func) { const accountPickle = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT); func(accountPickle); } - storeAccount(txn, accountPickle) { setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle); } - getCrossSigningKeys(txn, func) { const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS); func(keys); } - getSecretStorePrivateKey(txn, func, type) { const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`); func(key); } - storeCrossSigningKeys(txn, keys) { setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys); } - storeSecretStorePrivateKey(txn, type, key) { setJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`, key); } - doTxn(mode, stores, func) { return Promise.resolve(func(null)); } - } - exports.LocalStorageCryptoStore = LocalStorageCryptoStore; - function getJsonItem(store, key) { try { // if the key is absent, store.getItem() returns null, and // JSON.parse(null) === null, so this returns null. return JSON.parse(store.getItem(key)); } catch (e) { - _logger.logger.log("Error: Failed to get key %s: %s", key, e.stack || e); - + _logger.logger.log("Error: Failed to get key %s: %s", key, e.message); _logger.logger.log(e.stack); } - return null; } - function setJsonItem(store, key, val) { store.setItem(key, JSON.stringify(val)); } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/memory-crypto-store.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/memory-crypto-store.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/memory-crypto-store.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/store/memory-crypto-store.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,61 +4,36 @@ value: true }); exports.MemoryCryptoStore = void 0; - var _logger = require("../../logger"); - var utils = _interopRequireWildcard(require("../../utils")); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * Internal module. in-memory storage for e2e. - * - * @module */ -/** - * @implements {module:crypto/store/base~CryptoStore} - */ class MemoryCryptoStore { constructor() { _defineProperty(this, "outgoingRoomKeyRequests", []); - _defineProperty(this, "account", null); - _defineProperty(this, "crossSigningKeys", null); - _defineProperty(this, "privateKeys", {}); - _defineProperty(this, "sessions", {}); - _defineProperty(this, "sessionProblems", {}); - _defineProperty(this, "notifiedErrorDevices", {}); - _defineProperty(this, "inboundGroupSessions", {}); - _defineProperty(this, "inboundGroupSessionsWithheld", {}); - _defineProperty(this, "deviceData", null); - _defineProperty(this, "rooms", {}); - _defineProperty(this, "sessionsNeedingBackup", {}); - _defineProperty(this, "sharedHistoryInboundGroupSessions", {}); - _defineProperty(this, "parkedSharedHistory", new Map()); } - // keyed by room ID /** @@ -66,104 +41,92 @@ * * This must be called before the store can be used. * - * @return {Promise} resolves to the store. + * @returns resolves to the store. */ async startup() { // No startup work to do for the memory store. return this; } + /** * Delete all data from this store. * - * @returns {Promise} Promise which resolves when the store has been cleared. + * @returns Promise which resolves when the store has been cleared. */ - - deleteAllData() { return Promise.resolve(); } + /** * Look for an existing outgoing room key request, and if none is found, * add a new one * - * @param {module:crypto/store/base~OutgoingRoomKeyRequest} request * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the + * @returns resolves to + * {@link OutgoingRoomKeyRequest}: either the * same instance as passed in, or the existing one. */ - - getOrAddOutgoingRoomKeyRequest(request) { const requestBody = request.requestBody; return utils.promiseTry(() => { // first see if we already have an entry for this request. const existing = this._getOutgoingRoomKeyRequest(requestBody); - if (existing) { // this entry matches the request - return it. _logger.logger.log(`already have key request outstanding for ` + `${requestBody.room_id} / ${requestBody.session_id}: ` + `not sending another`); - return existing; - } // we got to the end of the list without finding a match - // - add the new request. - + } + // we got to the end of the list without finding a match + // - add the new request. _logger.logger.log(`enqueueing key request for ${requestBody.room_id} / ` + requestBody.session_id); - this.outgoingRoomKeyRequests.push(request); return request; }); } + /** * Look for an existing room key request * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {Promise} resolves to the matching - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the matching + * {@link OutgoingRoomKeyRequest}, or null if * not found */ - - getOutgoingRoomKeyRequest(requestBody) { return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody)); } + /** * Looks for existing room key request, and returns the result synchronously. * * @internal * - * @param {module:crypto~RoomKeyRequestBody} requestBody - * existing request to look for + * @param requestBody - existing request to look for * - * @return {module:crypto/store/base~OutgoingRoomKeyRequest?} + * @returns * the matching request, or null if not found */ // eslint-disable-next-line @typescript-eslint/naming-convention - - _getOutgoingRoomKeyRequest(requestBody) { for (const existing of this.outgoingRoomKeyRequests) { if (utils.deepCompare(existing.requestBody, requestBody)) { return existing; } } - return null; } + /** * Look for room key requests by state * - * @param {Array} wantedStates list of acceptable states + * @param wantedStates - list of acceptable states * - * @return {Promise} resolves to the a - * {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if + * @returns resolves to the a + * {@link OutgoingRoomKeyRequest}, or null if * there are no pending requests in those states */ - - getOutgoingRoomKeyRequestByState(wantedStates) { for (const req of this.outgoingRoomKeyRequests) { for (const state of wantedStates) { @@ -172,23 +135,18 @@ } } } - return Promise.resolve(null); } + /** * - * @param {Number} wantedState - * @return {Promise>} All OutgoingRoomKeyRequests in state + * @returns All OutgoingRoomKeyRequests in state */ - - getAllOutgoingRoomKeyRequestsByState(wantedState) { return Promise.resolve(this.outgoingRoomKeyRequests.filter(r => r.state == wantedState)); } - getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) { const results = []; - for (const req of this.outgoingRoomKeyRequests) { for (const state of wantedStates) { if (req.state === state && req.recipients.some(recipient => recipient.userId === userId && recipient.deviceId === deviceId)) { @@ -196,113 +154,95 @@ } } } - return Promise.resolve(results); } + /** * Look for an existing room key request by id and state, and update it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in - * @param {Object} updates name/value map of updates to apply + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in + * @param updates - name/value map of updates to apply * - * @returns {Promise} resolves to - * {@link module:crypto/store/base~OutgoingRoomKeyRequest} + * @returns resolves to + * {@link OutgoingRoomKeyRequest} * updated request, or null if no matching row was found */ - - updateOutgoingRoomKeyRequest(requestId, expectedState, updates) { for (const req of this.outgoingRoomKeyRequests) { if (req.requestId !== requestId) { continue; } - if (req.state !== expectedState) { _logger.logger.warn(`Cannot update room key request from ${expectedState} ` + `as it was already updated to ${req.state}`); - return Promise.resolve(null); } - Object.assign(req, updates); return Promise.resolve(req); } - return Promise.resolve(null); } + /** * Look for an existing room key request by id and state, and delete it if * found * - * @param {string} requestId ID of request to update - * @param {number} expectedState state we expect to find the request in + * @param requestId - ID of request to update + * @param expectedState - state we expect to find the request in * - * @returns {Promise} resolves once the operation is completed + * @returns resolves once the operation is completed */ - - deleteOutgoingRoomKeyRequest(requestId, expectedState) { for (let i = 0; i < this.outgoingRoomKeyRequests.length; i++) { const req = this.outgoingRoomKeyRequests[i]; - if (req.requestId !== requestId) { continue; } - if (req.state != expectedState) { _logger.logger.warn(`Cannot delete room key request in state ${req.state} ` + `(expected ${expectedState})`); - return Promise.resolve(null); } - this.outgoingRoomKeyRequests.splice(i, 1); return Promise.resolve(req); } - return Promise.resolve(null); - } // Olm Account + } + // Olm Account getAccount(txn, func) { func(this.account); } - storeAccount(txn, accountPickle) { this.account = accountPickle; } - getCrossSigningKeys(txn, func) { func(this.crossSigningKeys); } - getSecretStorePrivateKey(txn, func, type) { const result = this.privateKeys[type]; func(result || null); } - storeCrossSigningKeys(txn, keys) { this.crossSigningKeys = keys; } - storeSecretStorePrivateKey(txn, type, key) { this.privateKeys[type] = key; - } // Olm Sessions + } + // Olm Sessions countEndToEndSessions(txn, func) { func(Object.keys(this.sessions).length); } - getEndToEndSession(deviceKey, sessionId, txn, func) { const deviceSessions = this.sessions[deviceKey] || {}; func(deviceSessions[sessionId] || null); } - getEndToEndSessions(deviceKey, txn, func) { func(this.sessions[deviceKey] || {}); } - getAllEndToEndSessions(txn, func) { Object.entries(this.sessions).forEach(([deviceKey, deviceSessions]) => { Object.entries(deviceSessions).forEach(([sessionId, session]) => { @@ -313,18 +253,14 @@ }); }); } - storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { let deviceSessions = this.sessions[deviceKey]; - if (deviceSessions === undefined) { deviceSessions = {}; this.sessions[deviceKey] = deviceSessions; } - deviceSessions[sessionId] = sessionInfo; } - async storeEndToEndSessionProblem(deviceKey, type, fixed) { const problems = this.sessionProblems[deviceKey] = this.sessionProblems[deviceKey] || []; problems.push({ @@ -336,16 +272,12 @@ return a.time - b.time; }); } - async getEndToEndSessionProblem(deviceKey, timestamp) { const problems = this.sessionProblems[deviceKey] || []; - if (!problems.length) { return null; } - const lastProblem = problems[problems.length - 1]; - for (const problem of problems) { if (problem.time > timestamp) { return Object.assign({}, problem, { @@ -353,100 +285,89 @@ }); } } - if (lastProblem.fixed) { return null; } else { return lastProblem; } } - async filterOutNotifiedErrorDevices(devices) { const notifiedErrorDevices = this.notifiedErrorDevices; const ret = []; - for (const device of devices) { const { userId, deviceInfo } = device; - if (userId in notifiedErrorDevices) { if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) { ret.push(device); - notifiedErrorDevices[userId][deviceInfo.deviceId] = true; + (0, utils.safeSet)(notifiedErrorDevices[userId], deviceInfo.deviceId, true); } } else { ret.push(device); - notifiedErrorDevices[userId] = { + (0, utils.safeSet)(notifiedErrorDevices, userId, { [deviceInfo.deviceId]: true - }; + }); } } - return ret; - } // Inbound Group Sessions + } + // Inbound Group Sessions getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) { - const k = senderCurve25519Key + '/' + sessionId; + const k = senderCurve25519Key + "/" + sessionId; func(this.inboundGroupSessions[k] || null, this.inboundGroupSessionsWithheld[k] || null); } - getAllEndToEndInboundGroupSessions(txn, func) { for (const key of Object.keys(this.inboundGroupSessions)) { // we can't use split, as the components we are trying to split out // might themselves contain '/' characters. We rely on the // senderKey being a (32-byte) curve25519 key, base64-encoded // (hence 43 characters long). + func({ senderKey: key.slice(0, 43), sessionId: key.slice(44), sessionData: this.inboundGroupSessions[key] }); } - func(null); } - addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { - const k = senderCurve25519Key + '/' + sessionId; - + const k = senderCurve25519Key + "/" + sessionId; if (this.inboundGroupSessions[k] === undefined) { this.inboundGroupSessions[k] = sessionData; } } - storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) { - this.inboundGroupSessions[senderCurve25519Key + '/' + sessionId] = sessionData; + this.inboundGroupSessions[senderCurve25519Key + "/" + sessionId] = sessionData; } - storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn) { - const k = senderCurve25519Key + '/' + sessionId; + const k = senderCurve25519Key + "/" + sessionId; this.inboundGroupSessionsWithheld[k] = sessionData; - } // Device Data + } + // Device Data getEndToEndDeviceData(txn, func) { func(this.deviceData); } - storeEndToEndDeviceData(deviceData, txn) { this.deviceData = deviceData; - } // E2E rooms + } + // E2E rooms storeEndToEndRoom(roomId, roomInfo, txn) { this.rooms[roomId] = roomInfo; } - getEndToEndRooms(txn, func) { func(this.rooms); } - getSessionsNeedingBackup(limit) { const sessions = []; - for (const session in this.sessionsNeedingBackup) { if (this.inboundGroupSessions[session]) { sessions.push({ @@ -454,65 +375,53 @@ sessionId: session.slice(44), sessionData: this.inboundGroupSessions[session] }); - if (limit && session.length >= limit) { break; } } } - return Promise.resolve(sessions); } - countSessionsNeedingBackup() { return Promise.resolve(Object.keys(this.sessionsNeedingBackup).length); } - unmarkSessionsNeedingBackup(sessions) { for (const session of sessions) { - const sessionKey = session.senderKey + '/' + session.sessionId; + const sessionKey = session.senderKey + "/" + session.sessionId; delete this.sessionsNeedingBackup[sessionKey]; } - return Promise.resolve(); } - markSessionsNeedingBackup(sessions) { for (const session of sessions) { - const sessionKey = session.senderKey + '/' + session.sessionId; + const sessionKey = session.senderKey + "/" + session.sessionId; this.sessionsNeedingBackup[sessionKey] = true; } - return Promise.resolve(); } - addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) { const sessions = this.sharedHistoryInboundGroupSessions[roomId] || []; sessions.push([senderKey, sessionId]); this.sharedHistoryInboundGroupSessions[roomId] = sessions; } - getSharedHistoryInboundGroupSessions(roomId) { return Promise.resolve(this.sharedHistoryInboundGroupSessions[roomId] || []); } - addParkedSharedHistory(roomId, parkedData) { const parked = this.parkedSharedHistory.get(roomId) ?? []; parked.push(parkedData); this.parkedSharedHistory.set(roomId, parked); } - takeParkedSharedHistory(roomId) { const parked = this.parkedSharedHistory.get(roomId) ?? []; this.parkedSharedHistory.delete(roomId); return Promise.resolve(parked); - } // Session key backups + } + // Session key backups doTxn(mode, stores, func) { return Promise.resolve(func(null)); } - } - exports.MemoryCryptoStore = MemoryCryptoStore; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Base.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Base.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Base.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Base.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,39 +4,29 @@ value: true }); exports.VerificationEvent = exports.VerificationBase = exports.SwitchStartEventError = void 0; - var _event = require("../../models/event"); - +var _event2 = require("../../@types/event"); var _logger = require("../../logger"); - var _deviceinfo = require("../deviceinfo"); - var _Error = require("./Error"); - var _CrossSigning = require("../CrossSigning"); - var _typedEventEmitter = require("../../models/typed-event-emitter"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const timeoutException = new Error("Verification timed out"); - class SwitchStartEventError extends Error { constructor(startEvent) { super(); this.startEvent = startEvent; } - } - exports.SwitchStartEventError = SwitchStartEventError; let VerificationEvent; exports.VerificationEvent = VerificationEvent; - (function (VerificationEvent) { VerificationEvent["Cancel"] = "cancel"; })(VerificationEvent || (exports.VerificationEvent = VerificationEvent = {})); - class VerificationBase extends _typedEventEmitter.TypedEventEmitter { /** * Base class for verification methods. @@ -48,21 +38,19 @@ * *

Subclasses must have a NAME class property.

* - * @class - * - * @param {Object} channel the verification channel to send verification messages over. + * @param channel - the verification channel to send verification messages over. * TODO: Channel types * - * @param {MatrixClient} baseApis base matrix api interface + * @param baseApis - base matrix api interface * - * @param {string} userId the user ID that is being verified + * @param userId - the user ID that is being verified * - * @param {string} deviceId the device ID that is being verified + * @param deviceId - the device ID that is being verified * - * @param {object} [startEvent] the m.key.verification.start event that + * @param startEvent - the m.key.verification.start event that * initiated this verification, if any * - * @param {object} [request] the key verification request object related to + * @param request - the key verification request object related to * this verification, if any */ constructor(channel, baseApis, userId, deviceId, startEvent, request) { @@ -73,30 +61,18 @@ this.deviceId = deviceId; this.startEvent = startEvent; this.request = request; - _defineProperty(this, "cancelled", false); - _defineProperty(this, "_done", false); - _defineProperty(this, "promise", null); - _defineProperty(this, "transactionTimeoutTimer", null); - _defineProperty(this, "expectedEvent", void 0); - _defineProperty(this, "resolve", void 0); - _defineProperty(this, "reject", void 0); - _defineProperty(this, "resolveEvent", void 0); - _defineProperty(this, "rejectEvent", void 0); - _defineProperty(this, "started", void 0); - _defineProperty(this, "doVerification", void 0); } - get initiatedByMe() { // if there is no start event yet, // we probably want to send it, @@ -104,27 +80,21 @@ if (!this.startEvent) { return true; } - const sender = this.startEvent.getSender(); const content = this.startEvent.getContent(); return sender === this.baseApis.getUserId() && content.from_device === this.baseApis.getDeviceId(); } - get hasBeenCancelled() { return this.cancelled; } - resetTimer() { _logger.logger.info("Refreshing/starting the verification transaction timeout timer"); - if (this.transactionTimeoutTimer !== null) { clearTimeout(this.transactionTimeoutTimer); } - this.transactionTimeoutTimer = setTimeout(() => { if (!this._done && !this.cancelled) { _logger.logger.info("Triggering verification timeout"); - this.cancel(timeoutException); } }, 10 * 60 * 1000); // 10 minutes @@ -136,39 +106,31 @@ this.transactionTimeoutTimer = null; } } - send(type, uncompletedContent) { return this.channel.send(type, uncompletedContent); } - waitForEvent(type) { if (this._done) { return Promise.reject(new Error("Verification is already done")); } - const existingEvent = this.request.getEventFromOtherParty(type); - if (existingEvent) { return Promise.resolve(existingEvent); } - this.expectedEvent = type; return new Promise((resolve, reject) => { this.resolveEvent = resolve; this.rejectEvent = reject; }); } - canSwitchStartEvent(event) { return false; } - switchStartEvent(event) { if (this.canSwitchStartEvent(event)) { _logger.logger.log("Verification Base: switching verification start event", { restartingFlow: !!this.rejectEvent }); - if (this.rejectEvent) { const reject = this.rejectEvent; this.rejectEvent = undefined; @@ -178,23 +140,22 @@ } } } - handleEvent(e) { if (this._done) { return; } else if (e.getType() === this.expectedEvent) { // if we receive an expected m.key.verification.done, then just // ignore it, since we don't need to do anything about it - if (this.expectedEvent !== "m.key.verification.done") { + if (this.expectedEvent !== _event2.EventType.KeyVerificationDone) { this.expectedEvent = undefined; this.rejectEvent = undefined; this.resetTimer(); - this.resolveEvent(e); + this.resolveEvent?.(e); } - } else if (e.getType() === "m.key.verification.cancel") { + } else if (e.getType() === _event2.EventType.KeyVerificationCancel) { const reject = this.reject; - this.reject = undefined; // there is only promise to reject if verify has been called - + this.reject = undefined; + // there is only promise to reject if verify has been called if (reject) { const content = e.getContent(); const { @@ -210,34 +171,27 @@ // after a refresh when the events haven't been stored in the cache yet. const exception = new Error("Unexpected message: expecting " + this.expectedEvent + " but got " + e.getType()); this.expectedEvent = undefined; - if (this.rejectEvent) { const reject = this.rejectEvent; this.rejectEvent = undefined; reject(exception); } - this.cancel(exception); } } - - done() { + async done() { this.endTimer(); // always kill the activity timer - if (!this._done) { this.request.onVerifierFinished(); - this.resolve(); + this.resolve?.(); return (0, _CrossSigning.requestKeysDuringVerification)(this.baseApis, this.userId, this.deviceId); } } - cancel(e) { this.endTimer(); // always kill the activity timer - if (!this._done) { this.cancelled = true; this.request.onVerifierCancelled(); - if (this.userId && this.deviceId) { // send a cancellation to the other user (if it wasn't // cancelled by the other user) @@ -246,29 +200,26 @@ this.send(timeoutEvent.getType(), timeoutEvent.getContent()); } else if (e instanceof _event.MatrixEvent) { const sender = e.getSender(); - if (sender !== this.userId) { const content = e.getContent(); - - if (e.getType() === "m.key.verification.cancel") { + if (e.getType() === _event2.EventType.KeyVerificationCancel) { content.code = content.code || "m.unknown"; content.reason = content.reason || content.body || "Unknown reason"; - this.send("m.key.verification.cancel", content); + this.send(_event2.EventType.KeyVerificationCancel, content); } else { - this.send("m.key.verification.cancel", { + this.send(_event2.EventType.KeyVerificationCancel, { code: "m.unknown", reason: content.body || "Unknown reason" }); } } } else { - this.send("m.key.verification.cancel", { + this.send(_event2.EventType.KeyVerificationCancel, { code: "m.unknown", reason: e.toString() }); } } - if (this.promise !== null) { // when we cancel without a promise, we end up with a promise // but no reject function. If cancel is called again, we'd error. @@ -277,21 +228,19 @@ // FIXME: this causes an "Uncaught promise" console message // if nothing ends up chaining this promise. this.promise = Promise.reject(e); - } // Also emit a 'cancel' event that the app can listen for to detect cancellation + } + // Also emit a 'cancel' event that the app can listen for to detect cancellation // before calling verify() - - this.emit(VerificationEvent.Cancel, e); } } + /** * Begin the key verification * - * @returns {Promise} Promise which resolves when the verification has + * @returns Promise which resolves when the verification has * completed. */ - - verify() { if (this.promise) return this.promise; this.promise = new Promise((resolve, reject) => { @@ -300,48 +249,38 @@ this.endTimer(); resolve(...args); }; - this.reject = e => { this._done = true; this.endTimer(); reject(e); }; }); - if (this.doVerification && !this.started) { this.started = true; this.resetTimer(); // restart the timeout - new Promise((resolve, reject) => { const crossSignId = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(this.userId)?.getId(); - if (crossSignId === this.deviceId) { reject(new Error("Device ID is the same as the cross-signing ID")); } - resolve(); }).then(() => this.doVerification()).then(this.done.bind(this), this.cancel.bind(this)); } - return this.promise; } - async verifyKeys(userId, keys, verifier) { // we try to verify all the keys that we're told about, but we might // not know about all of them, so keep track of the keys that we know // about, and ignore the rest const verifiedDevices = []; - for (const [keyId, keyInfo] of Object.entries(keys)) { - const deviceId = keyId.split(':', 2)[1]; + const deviceId = keyId.split(":", 2)[1]; const device = this.baseApis.getStoredDevice(userId, deviceId); - if (device) { verifier(keyId, device, keyInfo); verifiedDevices.push([deviceId, keyId, device.keys[keyId]]); } else { const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId); - if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { verifier(keyId, _deviceinfo.DeviceInfo.fromStorage({ keys: { @@ -353,37 +292,32 @@ _logger.logger.warn(`verification: Could not find device ${deviceId} to verify`); } } - } // if none of the keys could be verified, then error because the app - // should be informed about that - + } + // if none of the keys could be verified, then error because the app + // should be informed about that if (!verifiedDevices.length) { throw new Error("No devices could be verified"); } - - _logger.logger.info("Verification completed! Marking devices verified: ", verifiedDevices); // TODO: There should probably be a batch version of this, otherwise it's going + _logger.logger.info("Verification completed! Marking devices verified: ", verifiedDevices); + // TODO: There should probably be a batch version of this, otherwise it's going // to upload each signature in a separate API call which is silly because the // API supports as many signatures as you like. - - for (const [deviceId, keyId, key] of verifiedDevices) { await this.baseApis.crypto.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key }); - } // if one of the user's own devices is being marked as verified / unverified, + } + + // if one of the user's own devices is being marked as verified / unverified, // check the key backup status, since whether or not we use this depends on // whether it has a signature from a verified device - - if (userId == this.baseApis.credentials.userId) { await this.baseApis.checkKeyBackup(); } } - get events() { return undefined; } - } - exports.VerificationBase = VerificationBase; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Error.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Error.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Error.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/Error.js 2023-04-11 06:11:52.000000000 +0000 @@ -7,9 +7,8 @@ exports.errorFromEvent = errorFromEvent; exports.newUserCancelledError = exports.newUnknownMethodError = exports.newUnexpectedMessageError = exports.newTimeoutError = exports.newKeyMismatchError = exports.newInvalidMessageError = void 0; exports.newVerificationError = newVerificationError; - var _event = require("../../models/event"); - +var _event2 = require("../../@types/event"); /* Copyright 2018 - 2021 The Matrix.org Foundation C.I.C. @@ -28,66 +27,61 @@ /** * Error messages. - * - * @module crypto/verification/Error */ + function newVerificationError(code, reason, extraData) { const content = Object.assign({}, { code, reason }, extraData); return new _event.MatrixEvent({ - type: "m.key.verification.cancel", + type: _event2.EventType.KeyVerificationCancel, content }); } - function errorFactory(code, reason) { return function (extraData) { return newVerificationError(code, reason, extraData); }; } + /** * The verification was cancelled by the user. */ - - const newUserCancelledError = errorFactory("m.user", "Cancelled by user"); + /** * The verification timed out. */ - exports.newUserCancelledError = newUserCancelledError; const newTimeoutError = errorFactory("m.timeout", "Timed out"); + /** * An unknown method was selected. */ - exports.newTimeoutError = newTimeoutError; const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method"); + /** * An unexpected message was sent. */ - exports.newUnknownMethodError = newUnknownMethodError; const newUnexpectedMessageError = errorFactory("m.unexpected_message", "Unexpected message"); + /** * The key does not match. */ - exports.newUnexpectedMessageError = newUnexpectedMessageError; const newKeyMismatchError = errorFactory("m.key_mismatch", "Key mismatch"); + /** * An invalid message was sent. */ - exports.newKeyMismatchError = newKeyMismatchError; const newInvalidMessageError = errorFactory("m.invalid_message", "Invalid message"); exports.newInvalidMessageError = newInvalidMessageError; - function errorFromEvent(event) { const content = event.getContent(); - if (content) { const { code, diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/IllegalMethod.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/IllegalMethod.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/IllegalMethod.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/IllegalMethod.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,35 +4,26 @@ value: true }); exports.IllegalMethod = void 0; - var _Base = require("./Base"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -/** - * @class crypto/verification/IllegalMethod/IllegalMethod - * @extends {module:crypto/verification/Base} - */ +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } class IllegalMethod extends _Base.VerificationBase { constructor(...args) { super(...args); - _defineProperty(this, "doVerification", async () => { throw new Error("Verification is not possible with this method"); }); } - static factory(channel, baseApis, userId, deviceId, startEvent, request) { return new IllegalMethod(channel, baseApis, userId, deviceId, startEvent, request); - } // eslint-disable-next-line @typescript-eslint/naming-convention - + } + // eslint-disable-next-line @typescript-eslint/naming-convention static get NAME() { // Typically the name will be something else, but to complete // the contract we offer a default one here. return "org.matrix.illegal_method"; } - } - exports.IllegalMethod = IllegalMethod; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/QRCode.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/QRCode.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/QRCode.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/QRCode.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,64 +4,51 @@ value: true }); exports.SHOW_QR_CODE_METHOD = exports.SCAN_QR_CODE_METHOD = exports.ReciprocateQRCode = exports.QrCodeEvent = exports.QRCodeData = void 0; - var _Base = require("./Base"); - var _Error = require("./Error"); - var _olmlib = require("../olmlib"); - var _logger = require("../../logger"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1"; exports.SHOW_QR_CODE_METHOD = SHOW_QR_CODE_METHOD; const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1"; exports.SCAN_QR_CODE_METHOD = SCAN_QR_CODE_METHOD; let QrCodeEvent; exports.QrCodeEvent = QrCodeEvent; - (function (QrCodeEvent) { QrCodeEvent["ShowReciprocateQr"] = "show_reciprocate_qr"; })(QrCodeEvent || (exports.QrCodeEvent = QrCodeEvent = {})); - -/** - * @class crypto/verification/QRCode/ReciprocateQRCode - * @extends {module:crypto/verification/Base} - */ class ReciprocateQRCode extends _Base.VerificationBase { constructor(...args) { super(...args); - _defineProperty(this, "reciprocateQREvent", void 0); - _defineProperty(this, "doVerification", async () => { if (!this.startEvent) { // TODO: Support scanning QR codes throw new Error("It is not currently possible to start verification" + "with this method yet."); } - const { qrCodeData - } = this.request; // 1. check the secret - - if (this.startEvent.getContent()['secret'] !== qrCodeData.encodedSharedSecret) { + } = this.request; + // 1. check the secret + if (this.startEvent.getContent()["secret"] !== qrCodeData?.encodedSharedSecret) { throw (0, _Error.newKeyMismatchError)(); - } // 2. ask if other user shows shield as well - + } + // 2. ask if other user shows shield as well await new Promise((resolve, reject) => { this.reciprocateQREvent = { confirm: resolve, cancel: () => reject((0, _Error.newUserCancelledError)()) }; this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent); - }); // 3. determine key to sign / mark as trusted + }); + // 3. determine key to sign / mark as trusted const keys = {}; - - switch (qrCodeData.mode) { + switch (qrCodeData?.mode) { case Mode.VerifyOtherUser: { // add master key to keys to be signed, only if we're not doing self-verification @@ -69,77 +56,66 @@ keys[`ed25519:${masterKey}`] = masterKey; break; } - case Mode.VerifySelfTrusted: { const deviceId = this.request.targetDevice.deviceId; keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey; break; } - case Mode.VerifySelfUntrusted: { const masterKey = qrCodeData.myMasterKey; keys[`ed25519:${masterKey}`] = masterKey; break; } - } // 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED) - + } + // 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED) await this.verifyKeys(this.userId, keys, (keyId, device, keyInfo) => { // make sure the device has the expected keys const targetKey = keys[keyId]; if (!targetKey) throw (0, _Error.newKeyMismatchError)(); - if (keyInfo !== targetKey) { _logger.logger.error("key ID from key info does not match"); - throw (0, _Error.newKeyMismatchError)(); } - for (const deviceKeyId in device.keys) { if (!deviceKeyId.startsWith("ed25519")) continue; const deviceTargetKey = keys[deviceKeyId]; if (!deviceTargetKey) throw (0, _Error.newKeyMismatchError)(); - if (device.keys[deviceKeyId] !== deviceTargetKey) { _logger.logger.error("master key does not match"); - throw (0, _Error.newKeyMismatchError)(); } } }); }); } - static factory(channel, baseApis, userId, deviceId, startEvent, request) { return new ReciprocateQRCode(channel, baseApis, userId, deviceId, startEvent, request); - } // eslint-disable-next-line @typescript-eslint/naming-convention - + } + // eslint-disable-next-line @typescript-eslint/naming-convention static get NAME() { return "m.reciprocate.v1"; } - } - exports.ReciprocateQRCode = ReciprocateQRCode; const CODE_VERSION = 0x02; // the version of binary QR codes we support - const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format - -var Mode; - +var Mode; // We do not trust the master key (function (Mode) { Mode[Mode["VerifyOtherUser"] = 0] = "VerifyOtherUser"; Mode[Mode["VerifySelfTrusted"] = 1] = "VerifySelfTrusted"; Mode[Mode["VerifySelfUntrusted"] = 2] = "VerifySelfUntrusted"; })(Mode || (Mode = {})); - class QRCodeData { - constructor(mode, sharedSecret, // only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code - otherUserMasterKey, // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code - otherDeviceKey, // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code + constructor(mode, sharedSecret, + // only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code + otherUserMasterKey, + // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code + otherDeviceKey, + // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code myMasterKey, buffer) { this.mode = mode; this.sharedSecret = sharedSecret; @@ -148,14 +124,12 @@ this.myMasterKey = myMasterKey; this.buffer = buffer; } - static async create(request, client) { const sharedSecret = QRCodeData.generateSharedSecret(); const mode = QRCodeData.determineMode(request, client); let otherUserMasterKey = null; let otherDeviceKey = null; let myMasterKey = null; - if (mode === Mode.VerifyOtherUser) { const otherUserCrossSigningInfo = client.getStoredCrossSigningForUser(request.otherUserId); otherUserMasterKey = otherUserCrossSigningInfo.getId("master"); @@ -166,62 +140,49 @@ const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId); myMasterKey = myCrossSigningInfo.getId("master"); } - const qrData = QRCodeData.generateQrData(request, client, mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey); const buffer = QRCodeData.generateBuffer(qrData); return new QRCodeData(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer); } + /** * The unpadded base64 encoded shared secret. */ - - get encodedSharedSecret() { return this.sharedSecret; } - getBuffer() { return this.buffer; } - static generateSharedSecret() { const secretBytes = new Uint8Array(11); global.crypto.getRandomValues(secretBytes); return (0, _olmlib.encodeUnpaddedBase64)(secretBytes); } - static async getOtherDeviceKey(request, client) { const myUserId = client.getUserId(); const otherDevice = request.targetDevice; - const otherDeviceId = otherDevice ? otherDevice.deviceId : null; - const device = client.getStoredDevice(myUserId, otherDeviceId); - + const device = otherDevice.deviceId ? client.getStoredDevice(myUserId, otherDevice.deviceId) : undefined; if (!device) { - throw new Error("could not find device " + otherDeviceId); + throw new Error("could not find device " + otherDevice?.deviceId); } - return device.getFingerprint(); } - static determineMode(request, client) { const myUserId = client.getUserId(); const otherUserId = request.otherUserId; let mode = Mode.VerifyOtherUser; - if (myUserId === otherUserId) { // Mode changes depending on whether or not we trust the master cross signing key const myTrust = client.checkUserTrust(myUserId); - if (myTrust.isCrossSigningVerified()) { mode = Mode.VerifySelfTrusted; } else { mode = Mode.VerifySelfUntrusted; } } - return mode; } - static generateQrData(request, client, mode, encodedSharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey) { const myUserId = client.getUserId(); const transactionId = request.channel.transactionId; @@ -230,18 +191,17 @@ version: CODE_VERSION, mode, transactionId, - firstKeyB64: '', + firstKeyB64: "", // worked out shortly - secondKeyB64: '', + secondKeyB64: "", // worked out shortly secretB64: encodedSharedSecret }; const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId); - if (mode === Mode.VerifyOtherUser) { // First key is our master cross signing key - qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); // Second key is the other user's master cross signing key - + qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); + // Second key is the other user's master cross signing key qrData.secondKeyB64 = otherUserMasterKey; } else if (mode === Mode.VerifySelfTrusted) { // First key is our master cross signing key @@ -249,14 +209,12 @@ qrData.secondKeyB64 = otherDeviceKey; } else if (mode === Mode.VerifySelfUntrusted) { // First key is our device's key - qrData.firstKeyB64 = client.getDeviceEd25519Key(); // Second key is what we think our master cross signing key is - + qrData.firstKeyB64 = client.getDeviceEd25519Key(); + // Second key is what we think our master cross signing key is qrData.secondKeyB64 = myMasterKey; } - return qrData; } - static generateBuffer(qrData) { let buf = Buffer.alloc(0); // we'll concat our way through life @@ -264,26 +222,23 @@ const tmpBuf = Buffer.from([b]); buf = Buffer.concat([buf, tmpBuf]); }; - const appendInt = i => { const tmpBuf = Buffer.alloc(2); tmpBuf.writeInt16BE(i, 0); buf = Buffer.concat([buf, tmpBuf]); }; - const appendStr = (s, enc, withLengthPrefix = true) => { const tmpBuf = Buffer.from(s, enc); if (withLengthPrefix) appendInt(tmpBuf.byteLength); buf = Buffer.concat([buf, tmpBuf]); }; - const appendEncBase64 = b64 => { const b = (0, _olmlib.decodeBase64)(b64); const tmpBuf = Buffer.from(b); buf = Buffer.concat([buf, tmpBuf]); - }; // Actually build the buffer for the QR code - + }; + // Actually build the buffer for the QR code appendStr(qrData.prefix, "ascii", false); appendByte(qrData.version); appendByte(qrData.mode); @@ -293,7 +248,5 @@ appendEncBase64(qrData.secretB64); return buf; } - } - exports.QRCodeData = QRCodeData; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/InRoomChannel.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/InRoomChannel.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/InRoomChannel.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/InRoomChannel.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,239 +4,205 @@ value: true }); exports.InRoomRequests = exports.InRoomChannel = void 0; - var _VerificationRequest = require("./VerificationRequest"); - var _logger = require("../../../logger"); - var _event = require("../../../@types/event"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const MESSAGE_TYPE = _event.EventType.RoomMessage; const M_REFERENCE = "m.reference"; const M_RELATES_TO = "m.relates_to"; + /** * A key verification channel that sends verification events in the timeline of a room. * Uses the event id of the initial m.key.verification.request event as a transaction id. */ - class InRoomChannel { /** - * @param {MatrixClient} client the matrix client, to send messages with and get current user & device from. - * @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user. - * @param {string} userId id of user that the verification request is directed at, should be present in the room. + * @param client - the matrix client, to send messages with and get current user & device from. + * @param roomId - id of the room where verification events should be posted in, should be a DM with the given user. + * @param userId - id of user that the verification request is directed at, should be present in the room. */ - constructor(client, roomId, userId = null) { + constructor(client, roomId, userId) { this.client = client; this.roomId = roomId; this.userId = userId; - - _defineProperty(this, "requestEventId", null); + _defineProperty(this, "requestEventId", void 0); } - get receiveStartFromOtherDevices() { return true; } - /** The transaction id generated/used by this verification channel */ - + /** The transaction id generated/used by this verification channel */ get transactionId() { return this.requestEventId; } - static getOtherPartyUserId(event, client) { const type = InRoomChannel.getEventType(event); - if (type !== _VerificationRequest.REQUEST_TYPE) { return; } - const ownUserId = client.getUserId(); const sender = event.getSender(); const content = event.getContent(); const receiver = content.to; - if (sender === ownUserId) { return receiver; } else if (receiver === ownUserId) { return sender; } } + /** - * @param {MatrixEvent} event the event to get the timestamp of - * @return {number} the timestamp when the event was sent + * @param event - the event to get the timestamp of + * @returns the timestamp when the event was sent */ - - getTimestamp(event) { return event.getTs(); } + /** * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel - * @param {string} type the event type to check - * @returns {boolean} boolean flag + * @param type - the event type to check + * @returns boolean flag */ - - static canCreateRequest(type) { return type === _VerificationRequest.REQUEST_TYPE; } - canCreateRequest(type) { return InRoomChannel.canCreateRequest(type); } + /** * Extract the transaction id used by a given key verification event, if any - * @param {MatrixEvent} event the event - * @returns {string} the transaction id + * @param event - the event + * @returns the transaction id */ - - static getTransactionId(event) { if (InRoomChannel.getEventType(event) === _VerificationRequest.REQUEST_TYPE) { return event.getId(); } else { const relation = event.getRelation(); - - if (relation && relation.rel_type === M_REFERENCE) { + if (relation?.rel_type === M_REFERENCE) { return relation.event_id; } } } + /** * Checks whether this event is a well-formed key verification event. * This only does checks that don't rely on the current state of a potentially already channel * so we can prevent channels being created by invalid events. * `handleEvent` can do more checks and choose to ignore invalid events. - * @param {MatrixEvent} event the event to validate - * @param {MatrixClient} client the client to get the current user and device id from - * @returns {boolean} whether the event is valid and should be passed to handleEvent + * @param event - the event to validate + * @param client - the client to get the current user and device id from + * @returns whether the event is valid and should be passed to handleEvent */ - - static validateEvent(event, client) { const txnId = InRoomChannel.getTransactionId(event); - if (typeof txnId !== "string" || txnId.length === 0) { return false; } - const type = InRoomChannel.getEventType(event); - const content = event.getContent(); // from here on we're fairly sure that this is supposed to be - // part of a verification request, so be noisy when rejecting something + const content = event.getContent(); + // from here on we're fairly sure that this is supposed to be + // part of a verification request, so be noisy when rejecting something if (type === _VerificationRequest.REQUEST_TYPE) { if (!content || typeof content.to !== "string" || !content.to.length) { _logger.logger.log("InRoomChannel: validateEvent: " + "no valid to " + (content && content.to)); - return false; - } // ignore requests that are not direct to or sent by the syncing user - + } + // ignore requests that are not direct to or sent by the syncing user if (!InRoomChannel.getOtherPartyUserId(event, client)) { _logger.logger.log("InRoomChannel: validateEvent: " + `not directed to or sent by me: ${event.getSender()}` + `, ${content && content.to}`); - return false; } } - return _VerificationRequest.VerificationRequest.validateEvent(type, event, client); } + /** * As m.key.verification.request events are as m.room.message events with the InRoomChannel * to have a fallback message in non-supporting clients, we map the real event type * to the symbolic one to keep things in unison with ToDeviceChannel - * @param {MatrixEvent} event the event to get the type of - * @returns {string} the "symbolic" event type + * @param event - the event to get the type of + * @returns the "symbolic" event type */ - - static getEventType(event) { const type = event.getType(); - if (type === MESSAGE_TYPE) { const content = event.getContent(); - if (content) { const { msgtype } = content; - if (msgtype === _VerificationRequest.REQUEST_TYPE) { return _VerificationRequest.REQUEST_TYPE; } } } - if (type && type !== _VerificationRequest.REQUEST_TYPE) { return type; } else { return ""; } } + /** * Changes the state of the channel, request, and verifier in response to a key verification event. - * @param {MatrixEvent} event to handle - * @param {VerificationRequest} request the request to forward handling to - * @param {boolean} isLiveEvent whether this is an even received through sync or not - * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. + * @param event - to handle + * @param request - the request to forward handling to + * @param isLiveEvent - whether this is an even received through sync or not + * @returns a promise that resolves when any requests as an answer to the passed-in event are sent. */ - - - handleEvent(event, request, isLiveEvent = false) { + async handleEvent(event, request, isLiveEvent = false) { // prevent processing the same event multiple times, as under // some circumstances Room.timeline can get emitted twice for the same event if (request.hasEventId(event.getId())) { return; } - - const type = InRoomChannel.getEventType(event); // do validations that need state (roomId, userId), + const type = InRoomChannel.getEventType(event); + // do validations that need state (roomId, userId), // ignore if invalid if (event.getRoomId() !== this.roomId) { return; - } // set userId if not set already - - - if (this.userId === null) { + } + // set userId if not set already + if (!this.userId) { const userId = InRoomChannel.getOtherPartyUserId(event, this.client); - if (userId) { this.userId = userId; } - } // ignore events not sent by us or the other party - - + } + // ignore events not sent by us or the other party const ownUserId = this.client.getUserId(); const sender = event.getSender(); - - if (this.userId !== null) { + if (this.userId) { if (sender !== ownUserId && sender !== this.userId) { - _logger.logger.log(`InRoomChannel: ignoring verification event from ` + `non-participating sender ${sender}`); - + _logger.logger.log(`InRoomChannel: ignoring verification event from non-participating sender ${sender}`); return; } } - - if (this.requestEventId === null) { + if (!this.requestEventId) { this.requestEventId = InRoomChannel.getTransactionId(event); } - const isRemoteEcho = !!event.getUnsigned().transaction_id; const isSentByUs = event.getSender() === this.client.getUserId(); return request.handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs); } + /** * Adds the transaction id (relation) back to a received event * so it has the same format as returned by `completeContent` before sending. * The relation can not appear on the event content because of encryption, * relations are excluded from encryption. - * @param {MatrixEvent} event the received event - * @returns {Object} the content object with the relation added again + * @param event - the received event + * @returns the content object with the relation added again */ - - completedContentFromEvent(event) { // ensure m.related_to is included in e2ee rooms // as the field is excluded from encryption @@ -244,24 +210,21 @@ content[M_RELATES_TO] = event.getRelation(); return content; } + /** * Add all the fields to content needed for sending it over this channel. * This is public so verification methods (SAS uses this) can get the exact * content that will be sent independent of the used channel, * as they need to calculate the hash of it. - * @param {string} type the event type - * @param {object} content the (incomplete) content - * @returns {object} the complete content, as it will be sent. + * @param type - the event type + * @param content - the (incomplete) content + * @returns the complete content, as it will be sent. */ - - completeContent(type, content) { content = Object.assign({}, content); - if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.READY_TYPE || type === _VerificationRequest.START_TYPE) { content.from_device = this.client.getDeviceId(); } - if (type === _VerificationRequest.REQUEST_TYPE) { // type is mapped to m.room.message in the send method content = { @@ -277,105 +240,81 @@ event_id: this.transactionId }; } - return content; } + /** * Send an event over the channel with the content not having gone through `completeContent`. - * @param {string} type the event type - * @param {object} uncompletedContent the (incomplete) content - * @returns {Promise} the promise of the request + * @param type - the event type + * @param uncompletedContent - the (incomplete) content + * @returns the promise of the request */ - - send(type, uncompletedContent) { const content = this.completeContent(type, uncompletedContent); return this.sendCompleted(type, content); } + /** * Send an event over the channel with the content having gone through `completeContent` already. - * @param {string} type the event type - * @param {object} content - * @returns {Promise} the promise of the request + * @param type - the event type + * @returns the promise of the request */ - - async sendCompleted(type, content) { let sendType = type; - if (type === _VerificationRequest.REQUEST_TYPE) { sendType = MESSAGE_TYPE; } - const response = await this.client.sendEvent(this.roomId, sendType, content); - if (type === _VerificationRequest.REQUEST_TYPE) { this.requestEventId = response.event_id; } } - } - exports.InRoomChannel = InRoomChannel; - class InRoomRequests { constructor() { _defineProperty(this, "requestsByRoomId", new Map()); } - getRequest(event) { const roomId = event.getRoomId(); const txnId = InRoomChannel.getTransactionId(event); return this.getRequestByTxnId(roomId, txnId); } - getRequestByChannel(channel) { return this.getRequestByTxnId(channel.roomId, channel.transactionId); } - getRequestByTxnId(roomId, txnId) { const requestsByTxnId = this.requestsByRoomId.get(roomId); - if (requestsByTxnId) { return requestsByTxnId.get(txnId); } } - setRequest(event, request) { this.doSetRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request); } - setRequestByChannel(channel, request) { this.doSetRequest(channel.roomId, channel.transactionId, request); } - doSetRequest(roomId, txnId, request) { let requestsByTxnId = this.requestsByRoomId.get(roomId); - if (!requestsByTxnId) { requestsByTxnId = new Map(); this.requestsByRoomId.set(roomId, requestsByTxnId); } - requestsByTxnId.set(txnId, request); } - removeRequest(event) { const roomId = event.getRoomId(); const requestsByTxnId = this.requestsByRoomId.get(roomId); - if (requestsByTxnId) { requestsByTxnId.delete(InRoomChannel.getTransactionId(event)); - if (requestsByTxnId.size === 0) { this.requestsByRoomId.delete(roomId); } } } - findRequestInProgress(roomId) { const requestsByTxnId = this.requestsByRoomId.get(roomId); - if (requestsByTxnId) { for (const request of requestsByTxnId.values()) { if (request.pending) { @@ -384,7 +323,5 @@ } } } - } - exports.InRoomRequests = InRoomRequests; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/ToDeviceChannel.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/ToDeviceChannel.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/ToDeviceChannel.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/ToDeviceChannel.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,35 +4,28 @@ value: true }); exports.ToDeviceRequests = exports.ToDeviceChannel = void 0; - var _randomstring = require("../../../randomstring"); - var _logger = require("../../../logger"); - var _VerificationRequest = require("./VerificationRequest"); - var _Error = require("../Error"); - var _event = require("../../../models/event"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * A key verification channel that sends verification events over to_device messages. * Generates its own transaction ids. */ class ToDeviceChannel { // userId and devices of user we're about to verify - constructor(client, userId, devices, transactionId = null, deviceId = null) { + constructor(client, userId, devices, transactionId, deviceId) { this.client = client; this.userId = userId; this.devices = devices; this.transactionId = transactionId; this.deviceId = deviceId; - _defineProperty(this, "request", void 0); } - isToDevices(devices) { if (devices.length === this.devices.length) { for (const device of devices) { @@ -40,128 +33,105 @@ return false; } } - return true; } else { return false; } } - static getEventType(event) { return event.getType(); } + /** * Extract the transaction id used by a given key verification event, if any - * @param {MatrixEvent} event the event - * @returns {string} the transaction id + * @param event - the event + * @returns the transaction id */ - - static getTransactionId(event) { const content = event.getContent(); return content && content.transaction_id; } + /** * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel - * @param {string} type the event type to check - * @returns {boolean} boolean flag + * @param type - the event type to check + * @returns boolean flag */ - - static canCreateRequest(type) { return type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.START_TYPE; } - canCreateRequest(type) { return ToDeviceChannel.canCreateRequest(type); } + /** * Checks whether this event is a well-formed key verification event. * This only does checks that don't rely on the current state of a potentially already channel * so we can prevent channels being created by invalid events. * `handleEvent` can do more checks and choose to ignore invalid events. - * @param {MatrixEvent} event the event to validate - * @param {MatrixClient} client the client to get the current user and device id from - * @returns {boolean} whether the event is valid and should be passed to handleEvent + * @param event - the event to validate + * @param client - the client to get the current user and device id from + * @returns whether the event is valid and should be passed to handleEvent */ - - static validateEvent(event, client) { if (event.isCancelled()) { _logger.logger.warn("Ignoring flagged verification request from " + event.getSender()); - return false; } - const content = event.getContent(); - if (!content) { _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: no content"); - return false; } - if (!content.transaction_id) { _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id"); - return false; } - const type = event.getType(); - if (type === _VerificationRequest.REQUEST_TYPE) { if (!Number.isFinite(content.timestamp)) { _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp"); - return false; } - if (event.getSender() === client.getUserId() && content.from_device == client.getDeviceId()) { // ignore requests from ourselves, because it doesn't make sense for a // device to verify itself _logger.logger.warn("ToDeviceChannel.validateEvent: invalid: from own device"); - return false; } } - return _VerificationRequest.VerificationRequest.validateEvent(type, event, client); } + /** - * @param {MatrixEvent} event the event to get the timestamp of - * @return {number} the timestamp when the event was sent + * @param event - the event to get the timestamp of + * @returns the timestamp when the event was sent */ - - getTimestamp(event) { const content = event.getContent(); return content && content.timestamp; } + /** * Changes the state of the channel, request, and verifier in response to a key verification event. - * @param {MatrixEvent} event to handle - * @param {VerificationRequest} request the request to forward handling to - * @param {boolean} isLiveEvent whether this is an even received through sync or not - * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. + * @param event - to handle + * @param request - the request to forward handling to + * @param isLiveEvent - whether this is an even received through sync or not + * @returns a promise that resolves when any requests as an answer to the passed-in event are sent. */ - - async handleEvent(event, request, isLiveEvent = false) { const type = event.getType(); const content = event.getContent(); - if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.READY_TYPE || type === _VerificationRequest.START_TYPE) { if (!this.transactionId) { this.transactionId = content.transaction_id; } - - const deviceId = content.from_device; // adopt deviceId if not set before and valid - + const deviceId = content.from_device; + // adopt deviceId if not set before and valid if (!this.deviceId && this.devices.includes(deviceId)) { this.deviceId = deviceId; - } // if no device id or different from adopted one, cancel with sender - - + } + // if no device id or different from adopted one, cancel with sender if (!this.deviceId || this.deviceId !== deviceId) { // also check that message came from the device we sent the request to earlier on // and do send a cancel message to that device @@ -170,15 +140,13 @@ return this.sendToDevices(_VerificationRequest.CANCEL_TYPE, cancelContent, [deviceId]); } } - const wasStarted = request.phase === _VerificationRequest.PHASE_STARTED || request.phase === _VerificationRequest.PHASE_READY; await request.handleEvent(event.getType(), event, isLiveEvent, false, false); const isStarted = request.phase === _VerificationRequest.PHASE_STARTED || request.phase === _VerificationRequest.PHASE_READY; - const isAcceptingEvent = type === _VerificationRequest.START_TYPE || type === _VerificationRequest.READY_TYPE; // the request has picked a ready or start event, tell the other devices about it - + const isAcceptingEvent = type === _VerificationRequest.START_TYPE || type === _VerificationRequest.READY_TYPE; + // the request has picked a ready or start event, tell the other devices about it if (isAcceptingEvent && !wasStarted && isStarted && this.deviceId) { const nonChosenDevices = this.devices.filter(d => d !== this.deviceId && d !== this.client.getDeviceId()); - if (nonChosenDevices.length) { const message = this.completeContent(_VerificationRequest.CANCEL_TYPE, { code: "m.accepted", @@ -188,179 +156,138 @@ } } } + /** - * See {InRoomChannel.completedContentFromEvent} why this is needed. - * @param {MatrixEvent} event the received event - * @returns {Object} the content object + * See {@link InRoomChannel#completedContentFromEvent} for why this is needed. + * @param event - the received event + * @returns the content object */ - - completedContentFromEvent(event) { return event.getContent(); } + /** * Add all the fields to content needed for sending it over this channel. * This is public so verification methods (SAS uses this) can get the exact * content that will be sent independent of the used channel, * as they need to calculate the hash of it. - * @param {string} type the event type - * @param {object} content the (incomplete) content - * @returns {object} the complete content, as it will be sent. + * @param type - the event type + * @param content - the (incomplete) content + * @returns the complete content, as it will be sent. */ - - completeContent(type, content) { // make a copy content = Object.assign({}, content); - if (this.transactionId) { content.transaction_id = this.transactionId; } - if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.READY_TYPE || type === _VerificationRequest.START_TYPE) { content.from_device = this.client.getDeviceId(); } - if (type === _VerificationRequest.REQUEST_TYPE) { content.timestamp = Date.now(); } - return content; } + /** * Send an event over the channel with the content not having gone through `completeContent`. - * @param {string} type the event type - * @param {object} uncompletedContent the (incomplete) content - * @returns {Promise} the promise of the request + * @param type - the event type + * @param uncompletedContent - the (incomplete) content + * @returns the promise of the request */ - - send(type, uncompletedContent = {}) { // create transaction id when sending request if ((type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.START_TYPE) && !this.transactionId) { this.transactionId = ToDeviceChannel.makeTransactionId(); } - const content = this.completeContent(type, uncompletedContent); return this.sendCompleted(type, content); } + /** * Send an event over the channel with the content having gone through `completeContent` already. - * @param {string} type the event type - * @param {object} content - * @returns {Promise} the promise of the request + * @param type - the event type + * @returns the promise of the request */ - - async sendCompleted(type, content) { let result; - if (type === _VerificationRequest.REQUEST_TYPE || type === _VerificationRequest.CANCEL_TYPE && !this.deviceId) { result = await this.sendToDevices(type, content, this.devices); } else { result = await this.sendToDevices(type, content, [this.deviceId]); - } // the VerificationRequest state machine requires remote echos of the event + } + // the VerificationRequest state machine requires remote echos of the event // the client sends itself, so we fake this for to_device messages - - const remoteEchoEvent = new _event.MatrixEvent({ sender: this.client.getUserId(), content, type }); - await this.request.handleEvent(type, remoteEchoEvent, - /*isLiveEvent=*/ - true, - /*isRemoteEcho=*/ - true, - /*isSentByUs=*/ - true); + await this.request.handleEvent(type, remoteEchoEvent, /*isLiveEvent=*/true, /*isRemoteEcho=*/true, /*isSentByUs=*/true); return result; } - async sendToDevices(type, content, devices) { if (devices.length) { - const msgMap = {}; - + const deviceMessages = new Map(); for (const deviceId of devices) { - msgMap[deviceId] = content; + deviceMessages.set(deviceId, content); } - - await this.client.sendToDevice(type, { - [this.userId]: msgMap - }); + await this.client.sendToDevice(type, new Map([[this.userId, deviceMessages]])); } } + /** * Allow Crypto module to create and know the transaction id before the .start event gets sent. - * @returns {string} the transaction id + * @returns the transaction id */ - - static makeTransactionId() { return (0, _randomstring.randomString)(32); } - } - exports.ToDeviceChannel = ToDeviceChannel; - class ToDeviceRequests { constructor() { _defineProperty(this, "requestsByUserId", new Map()); } - getRequest(event) { return this.getRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event)); } - getRequestByChannel(channel) { return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId); } - getRequestBySenderAndTxnId(sender, txnId) { const requestsByTxnId = this.requestsByUserId.get(sender); - if (requestsByTxnId) { return requestsByTxnId.get(txnId); } } - setRequest(event, request) { this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request); } - setRequestByChannel(channel, request) { this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request); } - setRequestBySenderAndTxnId(sender, txnId, request) { let requestsByTxnId = this.requestsByUserId.get(sender); - if (!requestsByTxnId) { requestsByTxnId = new Map(); this.requestsByUserId.set(sender, requestsByTxnId); } - requestsByTxnId.set(txnId, request); } - removeRequest(event) { const userId = event.getSender(); const requestsByTxnId = this.requestsByUserId.get(userId); - if (requestsByTxnId) { requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event)); - if (requestsByTxnId.size === 0) { this.requestsByUserId.delete(userId); } } } - findRequestInProgress(userId, devices) { const requestsByTxnId = this.requestsByUserId.get(userId); - if (requestsByTxnId) { for (const request of requestsByTxnId.values()) { if (request.pending && request.channel.isToDevices(devices)) { @@ -369,17 +296,12 @@ } } } - getRequestsInProgress(userId) { const requestsByTxnId = this.requestsByUserId.get(userId); - if (requestsByTxnId) { return Array.from(requestsByTxnId.values()).filter(r => r.pending); } - return []; } - } - exports.ToDeviceRequests = ToDeviceRequests; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/VerificationRequest.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/VerificationRequest.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/VerificationRequest.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/request/VerificationRequest.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,27 +4,24 @@ value: true }); exports.VerificationRequestEvent = exports.VerificationRequest = exports.START_TYPE = exports.REQUEST_TYPE = exports.READY_TYPE = exports.Phase = exports.PHASE_UNSENT = exports.PHASE_STARTED = exports.PHASE_REQUESTED = exports.PHASE_READY = exports.PHASE_DONE = exports.PHASE_CANCELLED = exports.EVENT_PREFIX = exports.DONE_TYPE = exports.CANCEL_TYPE = void 0; - var _logger = require("../../../logger"); - var _Error = require("../Error"); - var _QRCode = require("../QRCode"); - +var _event = require("../../../@types/event"); var _typedEventEmitter = require("../../../models/typed-event-emitter"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // How long after the event's timestamp that the request times out const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes -// How long after we receive the event that the request times out +// How long after we receive the event that the request times out const TIMEOUT_FROM_EVENT_RECEIPT = 2 * 60 * 1000; // 2 minutes + // to avoid almost expired verification notifications // from showing a notification and almost immediately // disappearing, also ignore verification requests that // are this amount of time away from expiring. - const VERIFICATION_REQUEST_MARGIN = 3 * 1000; // 3 seconds const EVENT_PREFIX = "m.key.verification."; @@ -40,9 +37,7 @@ const READY_TYPE = EVENT_PREFIX + "ready"; exports.READY_TYPE = READY_TYPE; let Phase; // Legacy export fields - exports.Phase = Phase; - (function (Phase) { Phase[Phase["Unsent"] = 1] = "Unsent"; Phase[Phase["Requested"] = 2] = "Requested"; @@ -51,7 +46,6 @@ Phase[Phase["Cancelled"] = 5] = "Cancelled"; Phase[Phase["Done"] = 6] = "Done"; })(Phase || (exports.Phase = Phase = {})); - const PHASE_UNSENT = Phase.Unsent; exports.PHASE_UNSENT = PHASE_UNSENT; const PHASE_REQUESTED = Phase.Requested; @@ -66,60 +60,44 @@ exports.PHASE_DONE = PHASE_DONE; let VerificationRequestEvent; exports.VerificationRequestEvent = VerificationRequestEvent; - (function (VerificationRequestEvent) { VerificationRequestEvent["Change"] = "change"; })(VerificationRequestEvent || (exports.VerificationRequestEvent = VerificationRequestEvent = {})); - /** * State machine for verification requests. * Things that differ based on what channel is used to * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`. - * @event "change" whenever the state of the request object has changed. */ class VerificationRequest extends _typedEventEmitter.TypedEventEmitter { // we keep a copy of the QR Code data (including other user master key) around // for QR reciprocate verification, to protect against // cross-signing identity reset between the .ready and .start event // and signing the wrong key after .start + // The timestamp when we received the request event from the other side + // Used in tests only + constructor(channel, verificationMethods, client) { super(); this.channel = channel; this.verificationMethods = verificationMethods; this.client = client; - _defineProperty(this, "eventsByUs", new Map()); - _defineProperty(this, "eventsByThem", new Map()); - _defineProperty(this, "_observeOnly", false); - _defineProperty(this, "timeoutTimer", null); - _defineProperty(this, "_accepting", false); - _defineProperty(this, "_declining", false); - _defineProperty(this, "verifierHasFinished", false); - _defineProperty(this, "_cancelled", false); - _defineProperty(this, "_chosenMethod", null); - _defineProperty(this, "_qrCodeData", null); - _defineProperty(this, "requestReceivedAt", null); - _defineProperty(this, "commonMethods", []); - _defineProperty(this, "_phase", void 0); - _defineProperty(this, "_cancellingUserId", void 0); - _defineProperty(this, "_verifier", void 0); - _defineProperty(this, "cancelOnTimeout", async () => { try { if (this.initiatedByMe) { @@ -137,183 +115,149 @@ _logger.logger.error("Error while cancelling verification request", err); } }); - this.channel.request = this; this.setPhase(PHASE_UNSENT, false); } + /** * Stateless validation logic not specific to the channel. * Invoked by the same static method in either channel. - * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. - * @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead. - * @param {MatrixClient} client the client to get the current user and device id from - * @returns {boolean} whether the event is valid and should be passed to handleEvent + * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel. + * @param event - the event to validate. Don't call getType() on it but use the `type` parameter instead. + * @param client - the client to get the current user and device id from + * @returns whether the event is valid and should be passed to handleEvent */ - - static validateEvent(type, event, client) { const content = event.getContent(); - if (!type || !type.startsWith(EVENT_PREFIX)) { return false; - } // from here on we're fairly sure that this is supposed to be - // part of a verification request, so be noisy when rejecting something - + } + // from here on we're fairly sure that this is supposed to be + // part of a verification request, so be noisy when rejecting something if (!content) { _logger.logger.log("VerificationRequest: validateEvent: no content"); - return false; } - if (type === REQUEST_TYPE || type === READY_TYPE) { if (!Array.isArray(content.methods)) { _logger.logger.log("VerificationRequest: validateEvent: " + "fail because methods"); - return false; } } - if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) { if (typeof content.from_device !== "string" || content.from_device.length === 0) { _logger.logger.log("VerificationRequest: validateEvent: " + "fail because from_device"); - return false; } } - return true; } - get invalid() { return this.phase === PHASE_UNSENT; } - /** returns whether the phase is PHASE_REQUESTED */ - + /** returns whether the phase is PHASE_REQUESTED */ get requested() { return this.phase === PHASE_REQUESTED; } - /** returns whether the phase is PHASE_CANCELLED */ - + /** returns whether the phase is PHASE_CANCELLED */ get cancelled() { return this.phase === PHASE_CANCELLED; } - /** returns whether the phase is PHASE_READY */ - + /** returns whether the phase is PHASE_READY */ get ready() { return this.phase === PHASE_READY; } - /** returns whether the phase is PHASE_STARTED */ - + /** returns whether the phase is PHASE_STARTED */ get started() { return this.phase === PHASE_STARTED; } - /** returns whether the phase is PHASE_DONE */ - + /** returns whether the phase is PHASE_DONE */ get done() { return this.phase === PHASE_DONE; } - /** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */ - + /** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */ get methods() { return this.commonMethods; } - /** the method picked in the .start event */ - + /** the method picked in the .start event */ get chosenMethod() { return this._chosenMethod; } - calculateEventTimeout(event) { let effectiveExpiresAt = this.channel.getTimestamp(event) + TIMEOUT_FROM_EVENT_TS; - if (this.requestReceivedAt && !this.initiatedByMe && this.phase <= PHASE_REQUESTED) { const expiresAtByReceipt = this.requestReceivedAt + TIMEOUT_FROM_EVENT_RECEIPT; effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt); } - return Math.max(0, effectiveExpiresAt - Date.now()); } - /** The current remaining amount of ms before the request should be automatically cancelled */ - + /** The current remaining amount of ms before the request should be automatically cancelled */ get timeout() { const requestEvent = this.getEventByEither(REQUEST_TYPE); - if (requestEvent) { return this.calculateEventTimeout(requestEvent); } - return 0; } + /** * The key verification request event. - * @returns {MatrixEvent} The request event, or falsey if not found. + * @returns The request event, or falsey if not found. */ - - get requestEvent() { return this.getEventByEither(REQUEST_TYPE); } - /** current phase of the request. Some properties might only be defined in a current phase. */ - + /** current phase of the request. Some properties might only be defined in a current phase. */ get phase() { return this._phase; } - /** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */ - + /** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */ get verifier() { return this._verifier; } - get canAccept() { return this.phase < PHASE_READY && !this._accepting && !this._declining; } - get accepting() { return this._accepting; } - get declining() { return this._declining; } - /** whether this request has sent it's initial event and needs more events to complete */ - + /** whether this request has sent it's initial event and needs more events to complete */ get pending() { return !this.observeOnly && this._phase !== PHASE_DONE && this._phase !== PHASE_CANCELLED; } - /** Only set after a .ready if the other party can scan a QR code */ - + /** Only set after a .ready if the other party can scan a QR code */ get qrCodeData() { return this._qrCodeData; } + /** Checks whether the other party supports a given verification method. * This is useful when setting up the QR code UI, as it is somewhat asymmetrical: * if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa. * For methods that need to be supported by both ends, use the `methods` property. - * @param {string} method the method to check - * @param {boolean} force to check even if the phase is not ready or started yet, internal usage - * @return {boolean} whether or not the other party said the supported the method */ - - + * @param method - the method to check + * @param force - to check even if the phase is not ready or started yet, internal usage + * @returns whether or not the other party said the supported the method */ otherPartySupportsMethod(method, force = false) { if (!force && !this.ready && !this.started) { return false; } - const theirMethodEvent = this.eventsByThem.get(REQUEST_TYPE) || this.eventsByThem.get(READY_TYPE); - if (!theirMethodEvent) { // if we started straight away with .start event, // we are assuming that the other side will support the @@ -324,63 +268,48 @@ const myStartMethod = content && content.method; return method == myStartMethod; } - return false; } - const content = theirMethodEvent.getContent(); - if (!content) { return false; } - const { methods } = content; - if (!Array.isArray(methods)) { return false; } - return methods.includes(method); } + /** Whether this request was initiated by the syncing user. * For InRoomChannel, this is who sent the .request event. * For ToDeviceChannel, this is who sent the .start event */ - - get initiatedByMe() { // event created by us but no remote echo has been received yet const noEventsYet = this.eventsByUs.size + this.eventsByThem.size === 0; - if (this._phase === PHASE_UNSENT && noEventsYet) { return true; } - const hasMyRequest = this.eventsByUs.has(REQUEST_TYPE); const hasTheirRequest = this.eventsByThem.has(REQUEST_TYPE); - if (hasMyRequest && !hasTheirRequest) { return true; } - if (!hasMyRequest && hasTheirRequest) { return false; } - const hasMyStart = this.eventsByUs.has(START_TYPE); const hasTheirStart = this.eventsByThem.has(START_TYPE); - if (hasMyStart && !hasTheirStart) { return true; } - return false; } - /** The id of the user that initiated the request */ - + /** The id of the user that initiated the request */ get requestingUserId() { if (this.initiatedByMe) { return this.client.getUserId(); @@ -388,9 +317,8 @@ return this.otherUserId; } } - /** The id of the user that (will) receive(d) the request */ - + /** The id of the user that (will) receive(d) the request */ get receivingUserId() { if (this.initiatedByMe) { return this.otherUserId; @@ -398,106 +326,90 @@ return this.client.getUserId(); } } - /** The user id of the other party in this request */ - + /** The user id of the other party in this request */ get otherUserId() { return this.channel.userId; } - get isSelfVerification() { return this.client.getUserId() === this.otherUserId; } + /** * The id of the user that cancelled the request, * only defined when phase is PHASE_CANCELLED */ - - get cancellingUserId() { const myCancel = this.eventsByUs.get(CANCEL_TYPE); const theirCancel = this.eventsByThem.get(CANCEL_TYPE); - if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) { return myCancel.getSender(); } - if (theirCancel) { return theirCancel.getSender(); } - return undefined; } + /** * The cancellation code e.g m.user which is responsible for cancelling this verification */ - - get cancellationCode() { const ev = this.getEventByEither(CANCEL_TYPE); return ev ? ev.getContent().code : null; } - get observeOnly() { return this._observeOnly; } + /** * Gets which device the verification should be started with * given the events sent so far in the verification. This is the * same algorithm used to determine which device to send the * verification to when no specific device is specified. - * @returns {{userId: *, deviceId: *}} The device information + * @returns The device information */ - - get targetDevice() { const theirFirstEvent = this.eventsByThem.get(REQUEST_TYPE) || this.eventsByThem.get(READY_TYPE) || this.eventsByThem.get(START_TYPE); - const theirFirstContent = theirFirstEvent.getContent(); - const fromDevice = theirFirstContent.from_device; + const theirFirstContent = theirFirstEvent?.getContent(); + const fromDevice = theirFirstContent?.from_device; return { userId: this.otherUserId, deviceId: fromDevice }; } + /* Start the key verification, creating a verifier and sending a .start event. * If no previous events have been sent, pass in `targetDevice` to set who to direct this request to. - * @param {string} method the name of the verification method to use. - * @param {string?} targetDevice.userId the id of the user to direct this request to - * @param {string?} targetDevice.deviceId the id of the device to direct this request to - * @returns {VerifierBase} the verifier of the given method + * @param method - the name of the verification method to use. + * @param targetDevice.userId the id of the user to direct this request to + * @param targetDevice.deviceId the id of the device to direct this request to + * @returns the verifier of the given method */ - - beginKeyVerification(method, targetDevice = null) { // need to allow also when unsent in case of to_device if (!this.observeOnly && !this._verifier) { const validStartPhase = this.phase === PHASE_REQUESTED || this.phase === PHASE_READY || this.phase === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE); - if (validStartPhase) { // when called on a request that was initiated with .request event // check the method is supported by both sides if (this.commonMethods.length && !this.commonMethods.includes(method)) { throw (0, _Error.newUnknownMethodError)(); } - this._verifier = this.createVerifier(method, null, targetDevice); - if (!this._verifier) { throw (0, _Error.newUnknownMethodError)(); } - this._chosenMethod = method; } } - return this._verifier; } + /** * sends the initial .request event. - * @returns {Promise} resolves when the event has been sent. + * @returns resolves when the event has been sent. */ - - async sendRequest() { if (!this.observeOnly && this._phase === PHASE_UNSENT) { const methods = [...this.verificationMethods.keys()]; @@ -506,14 +418,13 @@ }); } } + /** * Cancels the request, sending a cancellation to the other party - * @param {string?} error.reason the error reason to send the cancellation with - * @param {string?} error.code the error code to send the cancellation with - * @returns {Promise} resolves when the event has been sent. + * @param reason - the error reason to send the cancellation with + * @param code - the error code to send the cancellation with + * @returns resolves when the event has been sent. */ - - async cancel({ reason = "User declined", code = "m.user" @@ -521,7 +432,6 @@ if (!this.observeOnly && this._phase !== PHASE_CANCELLED) { this._declining = true; this.emit(VerificationRequestEvent.Change); - if (this._verifier) { return this._verifier.cancel((0, _Error.errorFactory)(code, reason)()); } else { @@ -533,12 +443,11 @@ } } } + /** * Accepts the request, sending a .ready event to the other party - * @returns {Promise} resolves when the event has been sent. + * @returns resolves when the event has been sent. */ - - async accept() { if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) { const methods = [...this.verificationMethods.keys()]; @@ -549,20 +458,18 @@ }); } } + /** * Can be used to listen for state changes until the callback returns true. - * @param {Function} fn callback to evaluate whether the request is in the desired state. + * @param fn - callback to evaluate whether the request is in the desired state. * Takes the request as an argument. - * @returns {Promise} that resolves once the callback returns true - * @throws {Error} when the request is cancelled + * @returns that resolves once the callback returns true + * @throws Error when the request is cancelled */ - - waitFor(fn) { return new Promise((resolve, reject) => { const check = () => { let handled = false; - if (fn(this)) { resolve(this); handled = true; @@ -570,32 +477,25 @@ reject(new Error("cancelled")); handled = true; } - if (handled) { this.off(VerificationRequestEvent.Change, check); } - return handled; }; - if (!check()) { this.on(VerificationRequestEvent.Change, check); } }); } - setPhase(phase, notify = true) { this._phase = phase; - if (notify) { this.emit(VerificationRequestEvent.Change); } } - getEventByEither(type) { return this.eventsByThem.get(type) || this.eventsByUs.get(type); } - getEventBy(type, byThem = false) { if (byThem) { return this.eventsByThem.get(type); @@ -603,40 +503,33 @@ return this.eventsByUs.get(type); } } - calculatePhaseTransitions() { const transitions = [{ phase: PHASE_UNSENT }]; + const phase = () => transitions[transitions.length - 1].phase; - const phase = () => transitions[transitions.length - 1].phase; // always pass by .request first to be sure channel.userId has been set - - + // always pass by .request first to be sure channel.userId has been set const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE); const requestEvent = this.getEventBy(REQUEST_TYPE, hasRequestByThem); - if (requestEvent) { transitions.push({ phase: PHASE_REQUESTED, event: requestEvent }); } - const readyEvent = requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem); - if (readyEvent && phase() === PHASE_REQUESTED) { transitions.push({ phase: PHASE_READY, event: readyEvent }); } - let startEvent; - if (readyEvent || !requestEvent) { const theirStartEvent = this.eventsByThem.get(START_TYPE); - const ourStartEvent = this.eventsByUs.get(START_TYPE); // any party can send .start after a .ready or unsent - + const ourStartEvent = this.eventsByUs.get(START_TYPE); + // any party can send .start after a .ready or unsent if (theirStartEvent && ourStartEvent) { startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ? theirStartEvent : ourStartEvent; } else { @@ -645,11 +538,9 @@ } else { startEvent = this.getEventBy(START_TYPE, !hasRequestByThem); } - if (startEvent) { - const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender(); + const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent?.getSender() !== startEvent.getSender(); const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE); - if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) { transitions.push({ phase: PHASE_STARTED, @@ -657,17 +548,13 @@ }); } } - const ourDoneEvent = this.eventsByUs.get(DONE_TYPE); - if (this.verifierHasFinished || ourDoneEvent && phase() === PHASE_STARTED) { transitions.push({ phase: PHASE_DONE }); } - const cancelEvent = this.getEventByEither(CANCEL_TYPE); - if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) { transitions.push({ phase: PHASE_CANCELLED, @@ -675,24 +562,21 @@ }); return transitions; } - return transitions; } - transitionToPhase(transition) { const { phase, event - } = transition; // get common methods - + } = transition; + // get common methods if (phase === PHASE_REQUESTED || phase === PHASE_READY) { if (!this.wasSentByOwnDevice(event)) { const content = event.getContent(); this.commonMethods = content.methods.filter(m => this.verificationMethods.has(m)); } - } // detect if we're not a party in the request, and we should just observe - - + } + // detect if we're not a party in the request, and we should just observe if (!this.observeOnly) { // if requested or accepted by one of my other devices if (phase === PHASE_REQUESTED || phase === PHASE_STARTED || phase === PHASE_READY) { @@ -700,17 +584,14 @@ this._observeOnly = true; } } - } // create verifier - - + } + // create verifier if (phase === PHASE_STARTED) { const { method } = event.getContent(); - if (!this._verifier && !this.observeOnly) { this._verifier = this.createVerifier(method, event); - if (!this._verifier) { this.cancel({ code: "m.unknown_method", @@ -722,28 +603,23 @@ } } } - applyPhaseTransitions() { const transitions = this.calculatePhaseTransitions(); - const existingIdx = transitions.findIndex(t => t.phase === this.phase); // trim off phases we already went through, if any - - const newTransitions = transitions.slice(existingIdx + 1); // transition to all new phases - + const existingIdx = transitions.findIndex(t => t.phase === this.phase); + // trim off phases we already went through, if any + const newTransitions = transitions.slice(existingIdx + 1); + // transition to all new phases for (const transition of newTransitions) { this.transitionToPhase(transition); } - return newTransitions; } - isWinningStartRace(newEvent) { if (newEvent.getType() !== START_TYPE) { return false; } - const oldEvent = this._verifier.startEvent; let oldRaceIdentifier; - if (this.isSelfVerification) { // if the verifier does not have a startEvent, // it is because it's still sending and we are on the initator side @@ -762,83 +638,71 @@ oldRaceIdentifier = this.client.getUserId(); } } - let newRaceIdentifier; - if (this.isSelfVerification) { const newContent = newEvent.getContent(); newRaceIdentifier = newContent && newContent.from_device; } else { newRaceIdentifier = newEvent.getSender(); } - return newRaceIdentifier < oldRaceIdentifier; } - hasEventId(eventId) { for (const event of this.eventsByUs.values()) { if (event.getId() === eventId) { return true; } } - for (const event of this.eventsByThem.values()) { if (event.getId() === eventId) { return true; } } - return false; } + /** * Changes the state of the request and verifier in response to a key verification event. - * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. - * @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead. - * @param {boolean} isLiveEvent whether this is an even received through sync or not - * @param {boolean} isRemoteEcho whether this is the remote echo of an event sent by the same device - * @param {boolean} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers. + * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel. + * @param event - the event to handle. Don't call getType() on it but use the `type` parameter instead. + * @param isLiveEvent - whether this is an even received through sync or not + * @param isRemoteEcho - whether this is the remote echo of an event sent by the same device + * @param isSentByUs - whether this event is sent by a party that can accept and/or observe the request like one of our peers. * For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device. - * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. + * @returns a promise that resolves when any requests as an answer to the passed-in event are sent. */ - - async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) { // if reached phase cancelled or done, ignore anything else that comes if (this.done || this.cancelled) { return; } - const wasObserveOnly = this._observeOnly; this.adjustObserveOnly(event, isLiveEvent); - if (!this.observeOnly && !isRemoteEcho) { if (await this.cancelOnError(type, event)) { return; } - } // This assumes verification won't need to send an event with + } + + // This assumes verification won't need to send an event with // the same type for the same party twice. // This is true for QR and SAS verification, and was // added here to prevent verification getting cancelled // when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365) - - const isDuplicateEvent = isSentByUs ? this.eventsByUs.has(type) : this.eventsByThem.has(type); - if (isDuplicateEvent) { return; } - const oldPhase = this.phase; - this.addEvent(type, event, isSentByUs); // this will create if needed the verifier so needs to happen before calling it + this.addEvent(type, event, isSentByUs); + // this will create if needed the verifier so needs to happen before calling it const newTransitions = this.applyPhaseTransitions(); - try { // only pass events from the other side to the verifier, // no remote echos of our own events if (this._verifier && !this.observeOnly) { const newEventWinsRace = this.isWinningStartRace(event); - if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) { this._verifier.switchStartEvent(event); } else if (!isRemoteEcho) { @@ -847,7 +711,6 @@ } } } - if (newTransitions.length) { // create QRCodeData if the other side can scan // important this happens before emitting a phase change, @@ -857,18 +720,16 @@ // we happen to have at some later point in time. if (isLiveEvent && newTransitions.some(t => t.phase === PHASE_READY)) { const shouldGenerateQrCode = this.otherPartySupportsMethod(_QRCode.SCAN_QR_CODE_METHOD, true); - if (shouldGenerateQrCode) { this._qrCodeData = await _QRCode.QRCodeData.create(this, this.client); } } - const lastTransition = newTransitions[newTransitions.length - 1]; const { phase } = lastTransition; - this.setupTimeout(phase); // set phase as last thing as this emits the "change" event - + this.setupTimeout(phase); + // set phase as last thing as this emits the "change" event this.setPhase(phase); } else if (this._observeOnly !== wasObserveOnly) { this.emit(VerificationRequestEvent.Change); @@ -878,150 +739,121 @@ _logger.logger.log(`Verification request ${this.channel.transactionId}: ` + `${type} event with id:${event.getId()}, ` + `content:${JSON.stringify(event.getContent())} ` + `deviceId:${this.channel.deviceId}, ` + `sender:${event.getSender()}, isSentByUs:${isSentByUs}, ` + `isLiveEvent:${isLiveEvent}, isRemoteEcho:${isRemoteEcho}, ` + `phase:${oldPhase}=>${this.phase}, ` + `observeOnly:${wasObserveOnly}=>${this._observeOnly}`); } } - setupTimeout(phase) { const shouldTimeout = !this.timeoutTimer && !this.observeOnly && phase === PHASE_REQUESTED; - if (shouldTimeout) { this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout); } - if (this.timeoutTimer) { const shouldClear = phase === PHASE_STARTED || phase === PHASE_READY || phase === PHASE_DONE || phase === PHASE_CANCELLED; - if (shouldClear) { clearTimeout(this.timeoutTimer); this.timeoutTimer = null; } } } - async cancelOnError(type, event) { if (type === START_TYPE) { const method = event.getContent().method; - if (!this.verificationMethods.has(method)) { await this.cancel((0, _Error.errorFromEvent)((0, _Error.newUnknownMethodError)())); return true; } } - const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT; - const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED && this.phase !== PHASE_STARTED; // only if phase has passed from PHASE_UNSENT should we cancel, because events + const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED && this.phase !== PHASE_STARTED; + // only if phase has passed from PHASE_UNSENT should we cancel, because events // are allowed to come in in any order (at least with InRoomChannel). So we only know // we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED. // Before that, we could be looking at somebody else's verification request and we just // happen to be in the room - if (this.phase !== PHASE_UNSENT && (isUnexpectedRequest || isUnexpectedReady)) { _logger.logger.warn(`Cancelling, unexpected ${type} verification ` + `event from ${event.getSender()}`); - const reason = `Unexpected ${type} event in phase ${this.phase}`; await this.cancel((0, _Error.errorFromEvent)((0, _Error.newUnexpectedMessageError)({ reason }))); return true; } - return false; } - adjustObserveOnly(event, isLiveEvent = false) { // don't send out events for historical requests if (!isLiveEvent) { this._observeOnly = true; } - if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) { this._observeOnly = true; } } - addEvent(type, event, isSentByUs = false) { if (isSentByUs) { this.eventsByUs.set(type, event); } else { this.eventsByThem.set(type, event); - } // once we know the userId of the other party (from the .request event) - // see if any event by anyone else crept into this.eventsByThem - + } + // once we know the userId of the other party (from the .request event) + // see if any event by anyone else crept into this.eventsByThem if (type === REQUEST_TYPE) { for (const [type, event] of this.eventsByThem.entries()) { if (event.getSender() !== this.otherUserId) { this.eventsByThem.delete(type); } - } // also remember when we received the request event - - + } + // also remember when we received the request event this.requestReceivedAt = Date.now(); } } - createVerifier(method, startEvent = null, targetDevice = null) { if (!targetDevice) { targetDevice = this.targetDevice; } - const { userId, deviceId } = targetDevice; const VerifierCtor = this.verificationMethods.get(method); - if (!VerifierCtor) { _logger.logger.warn("could not find verifier constructor for method", method); - return; } - return new VerifierCtor(this.channel, this.client, userId, deviceId, startEvent, this); } - wasSentByOwnUser(event) { - return event.getSender() === this.client.getUserId(); - } // only for .request, .ready or .start - + return event?.getSender() === this.client.getUserId(); + } + // only for .request, .ready or .start wasSentByOwnDevice(event) { if (!this.wasSentByOwnUser(event)) { return false; } - const content = event.getContent(); - if (!content || content.from_device !== this.client.getDeviceId()) { return false; } - return true; } - onVerifierCancelled() { - this._cancelled = true; // move to cancelled phase - + this._cancelled = true; + // move to cancelled phase const newTransitions = this.applyPhaseTransitions(); - if (newTransitions.length) { this.setPhase(newTransitions[newTransitions.length - 1].phase); } } - onVerifierFinished() { - this.channel.send("m.key.verification.done", {}); - this.verifierHasFinished = true; // move to .done phase - + this.channel.send(_event.EventType.KeyVerificationDone, {}); + this.verifierHasFinished = true; + // move to .done phase const newTransitions = this.applyPhaseTransitions(); - if (newTransitions.length) { this.setPhase(newTransitions[newTransitions.length - 1].phase); } } - getEventFromOtherParty(type) { return this.eventsByThem.get(type); } - } - exports.VerificationRequest = VerificationRequest; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SASDecimal.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SASDecimal.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SASDecimal.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SASDecimal.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,39 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.generateDecimalSas = generateDecimalSas; +/* +Copyright 2018 - 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Implementation of decimal encoding of SAS as per: + * https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal + * @param sasBytes - the five bytes generated by HKDF + * @returns the derived three numbers between 1000 and 9191 inclusive + */ +function generateDecimalSas(sasBytes) { + /* + * +--------+--------+--------+--------+--------+ + * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + * +--------+--------+--------+--------+--------+ + * bits: 87654321 87654321 87654321 87654321 87654321 + * \____________/\_____________/\____________/ + * 1st number 2nd number 3rd number + */ + return [(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000, ((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000, ((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000]; +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SAS.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SAS.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SAS.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/crypto/verification/SAS.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,143 +4,183 @@ value: true }); exports.SasEvent = exports.SAS = void 0; - var _anotherJson = _interopRequireDefault(require("another-json")); - var _Base = require("./Base"); - var _Error = require("./Error"); - var _logger = require("../../logger"); - +var _SASDecimal = require("./SASDecimal"); +var _event = require("../../@types/event"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -const START_TYPE = "m.key.verification.start"; -const EVENTS = ["m.key.verification.accept", "m.key.verification.key", "m.key.verification.mac"]; +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +const START_TYPE = _event.EventType.KeyVerificationStart; +const EVENTS = [_event.EventType.KeyVerificationAccept, _event.EventType.KeyVerificationKey, _event.EventType.KeyVerificationMac]; let olmutil; const newMismatchedSASError = (0, _Error.errorFactory)("m.mismatched_sas", "Mismatched short authentication string"); const newMismatchedCommitmentError = (0, _Error.errorFactory)("m.mismatched_commitment", "Mismatched commitment"); - -function generateDecimalSas(sasBytes) { - /** - * +--------+--------+--------+--------+--------+ - * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | - * +--------+--------+--------+--------+--------+ - * bits: 87654321 87654321 87654321 87654321 87654321 - * \____________/\_____________/\____________/ - * 1st number 2nd number 3rd number - */ - return [(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000, ((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000, ((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000]; -} - -const emojiMapping = [["🐶", "dog"], // 0 -["🐱", "cat"], // 1 -["🦁", "lion"], // 2 -["🐎", "horse"], // 3 -["🦄", "unicorn"], // 4 -["🐷", "pig"], // 5 -["🐘", "elephant"], // 6 -["🐰", "rabbit"], // 7 -["🐼", "panda"], // 8 -["🐓", "rooster"], // 9 -["🐧", "penguin"], // 10 -["🐢", "turtle"], // 11 -["🐟", "fish"], // 12 -["🐙", "octopus"], // 13 -["🦋", "butterfly"], // 14 -["🌷", "flower"], // 15 -["🌳", "tree"], // 16 -["🌵", "cactus"], // 17 -["🍄", "mushroom"], // 18 -["🌏", "globe"], // 19 -["🌙", "moon"], // 20 -["☁️", "cloud"], // 21 -["🔥", "fire"], // 22 -["🍌", "banana"], // 23 -["🍎", "apple"], // 24 -["🍓", "strawberry"], // 25 -["🌽", "corn"], // 26 -["🍕", "pizza"], // 27 -["🎂", "cake"], // 28 -["❤️", "heart"], // 29 -["🙂", "smiley"], // 30 -["🤖", "robot"], // 31 -["🎩", "hat"], // 32 -["👓", "glasses"], // 33 -["🔧", "spanner"], // 34 -["🎅", "santa"], // 35 -["👍", "thumbs up"], // 36 -["☂️", "umbrella"], // 37 -["⌛", "hourglass"], // 38 -["⏰", "clock"], // 39 -["🎁", "gift"], // 40 -["💡", "light bulb"], // 41 -["📕", "book"], // 42 -["✏️", "pencil"], // 43 -["📎", "paperclip"], // 44 -["✂️", "scissors"], // 45 -["🔒", "lock"], // 46 -["🔑", "key"], // 47 -["🔨", "hammer"], // 48 -["☎️", "telephone"], // 49 -["🏁", "flag"], // 50 -["🚂", "train"], // 51 -["🚲", "bicycle"], // 52 -["✈️", "aeroplane"], // 53 -["🚀", "rocket"], // 54 -["🏆", "trophy"], // 55 -["⚽", "ball"], // 56 -["🎸", "guitar"], // 57 -["🎺", "trumpet"], // 58 -["🔔", "bell"], // 59 -["⚓️", "anchor"], // 60 -["🎧", "headphones"], // 61 -["📁", "folder"], // 62 +const emojiMapping = [["🐶", "dog"], +// 0 +["🐱", "cat"], +// 1 +["🦁", "lion"], +// 2 +["🐎", "horse"], +// 3 +["🦄", "unicorn"], +// 4 +["🐷", "pig"], +// 5 +["🐘", "elephant"], +// 6 +["🐰", "rabbit"], +// 7 +["🐼", "panda"], +// 8 +["🐓", "rooster"], +// 9 +["🐧", "penguin"], +// 10 +["🐢", "turtle"], +// 11 +["🐟", "fish"], +// 12 +["🐙", "octopus"], +// 13 +["🦋", "butterfly"], +// 14 +["🌷", "flower"], +// 15 +["🌳", "tree"], +// 16 +["🌵", "cactus"], +// 17 +["🍄", "mushroom"], +// 18 +["🌏", "globe"], +// 19 +["🌙", "moon"], +// 20 +["☁️", "cloud"], +// 21 +["🔥", "fire"], +// 22 +["🍌", "banana"], +// 23 +["🍎", "apple"], +// 24 +["🍓", "strawberry"], +// 25 +["🌽", "corn"], +// 26 +["🍕", "pizza"], +// 27 +["🎂", "cake"], +// 28 +["❤️", "heart"], +// 29 +["🙂", "smiley"], +// 30 +["🤖", "robot"], +// 31 +["🎩", "hat"], +// 32 +["👓", "glasses"], +// 33 +["🔧", "spanner"], +// 34 +["🎅", "santa"], +// 35 +["👍", "thumbs up"], +// 36 +["☂️", "umbrella"], +// 37 +["⌛", "hourglass"], +// 38 +["⏰", "clock"], +// 39 +["🎁", "gift"], +// 40 +["💡", "light bulb"], +// 41 +["📕", "book"], +// 42 +["✏️", "pencil"], +// 43 +["📎", "paperclip"], +// 44 +["✂️", "scissors"], +// 45 +["🔒", "lock"], +// 46 +["🔑", "key"], +// 47 +["🔨", "hammer"], +// 48 +["☎️", "telephone"], +// 49 +["🏁", "flag"], +// 50 +["🚂", "train"], +// 51 +["🚲", "bicycle"], +// 52 +["✈️", "aeroplane"], +// 53 +["🚀", "rocket"], +// 54 +["🏆", "trophy"], +// 55 +["⚽", "ball"], +// 56 +["🎸", "guitar"], +// 57 +["🎺", "trumpet"], +// 58 +["🔔", "bell"], +// 59 +["⚓️", "anchor"], +// 60 +["🎧", "headphones"], +// 61 +["📁", "folder"], +// 62 ["📌", "pin"] // 63 ]; function generateEmojiSas(sasBytes) { - const emojis = [// just like base64 encoding + const emojis = [ + // just like base64 encoding sasBytes[0] >> 2, (sasBytes[0] & 0x3) << 4 | sasBytes[1] >> 4, (sasBytes[1] & 0xf) << 2 | sasBytes[2] >> 6, sasBytes[2] & 0x3f, sasBytes[3] >> 2, (sasBytes[3] & 0x3) << 4 | sasBytes[4] >> 4, (sasBytes[4] & 0xf) << 2 | sasBytes[5] >> 6]; return emojis.map(num => emojiMapping[num]); } - const sasGenerators = { - decimal: generateDecimalSas, + decimal: _SASDecimal.generateDecimalSas, emoji: generateEmojiSas }; - function generateSas(sasBytes, methods) { const sas = {}; - for (const method of methods) { if (method in sasGenerators) { - sas[method] = sasGenerators[method](sasBytes); + // @ts-ignore - ts doesn't like us mixing types like this + sas[method] = sasGenerators[method](Array.from(sasBytes)); } } - return sas; } - const macMethods = { "hkdf-hmac-sha256": "calculate_mac", "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64", + "hkdf-hmac-sha256.v2": "calculate_mac_fixed_base64", "hmac-sha256": "calculate_mac_long_kdf" }; - function calculateMAC(olmSAS, method) { - return function (...args) { - const macFunction = olmSAS[macMethods[method]]; - const mac = macFunction.apply(olmSAS, args); - - _logger.logger.log("SAS calculateMAC:", method, args, mac); - + return function (input, info) { + const mac = olmSAS[macMethods[method]](input, info); + _logger.logger.log("SAS calculateMAC:", method, [input, info], mac); return mac; }; } - const calculateKeyAgreement = { // eslint-disable-next-line @typescript-eslint/naming-convention "curve25519-hkdf-sha256": function (sas, olmSAS, bytes) { @@ -160,50 +200,36 @@ * and MAC lists should be sorted in order of preference (most preferred * first). */ - const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"]; const HASHES_LIST = ["sha256"]; -const MAC_LIST = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"]; +const MAC_LIST = ["hkdf-hmac-sha256.v2", "org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"]; const SAS_LIST = Object.keys(sasGenerators); const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST); const HASHES_SET = new Set(HASHES_LIST); const MAC_SET = new Set(MAC_LIST); const SAS_SET = new Set(SAS_LIST); - function intersection(anArray, aSet) { - return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : []; + return Array.isArray(anArray) ? anArray.filter(x => aSet.has(x)) : []; } - let SasEvent; exports.SasEvent = SasEvent; - (function (SasEvent) { SasEvent["ShowSas"] = "show_sas"; })(SasEvent || (exports.SasEvent = SasEvent = {})); - -/** - * @alias module:crypto/verification/SAS - * @extends {module:crypto/verification/Base} - */ class SAS extends _Base.VerificationBase { constructor(...args) { super(...args); - _defineProperty(this, "waitingForAccept", void 0); - _defineProperty(this, "ourSASPubKey", void 0); - _defineProperty(this, "theirSASPubKey", void 0); - _defineProperty(this, "sasEvent", void 0); - _defineProperty(this, "doVerification", async () => { await global.Olm.init(); - olmutil = olmutil || new global.Olm.Utility(); // make sure user's keys are downloaded + olmutil = olmutil || new global.Olm.Utility(); + // make sure user's keys are downloaded await this.baseApis.downloadKeys([this.userId]); let retry = false; - do { try { if (this.initiatedByMe) { @@ -223,25 +249,20 @@ } while (retry); }); } - // eslint-disable-next-line @typescript-eslint/naming-convention static get NAME() { return "m.sas.v1"; } - get events() { return EVENTS; } - canSwitchStartEvent(event) { if (event.getType() !== START_TYPE) { return false; } - const content = event.getContent(); - return content && content.method === SAS.NAME && this.waitingForAccept; + return content?.method === SAS.NAME && !!this.waitingForAccept; } - async sendStart() { const startContent = this.channel.completeContent(START_TYPE, { method: SAS.NAME, @@ -255,122 +276,109 @@ await this.channel.sendCompleted(START_TYPE, startContent); return startContent; } - + async verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod) { + const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); + const verifySAS = new Promise((resolve, reject) => { + this.sasEvent = { + sas: generateSas(sasBytes, sasMethods), + confirm: async () => { + try { + await this.sendMAC(olmSAS, macMethod); + resolve(); + } catch (err) { + reject(err); + } + }, + cancel: () => reject((0, _Error.newUserCancelledError)()), + mismatch: () => reject(newMismatchedSASError()) + }; + this.emit(SasEvent.ShowSas, this.sasEvent); + }); + const [e] = await Promise.all([this.waitForEvent(_event.EventType.KeyVerificationMac).then(e => { + // we don't expect any more messages from the other + // party, and they may send a m.key.verification.done + // when they're done on their end + this.expectedEvent = _event.EventType.KeyVerificationDone; + return e; + }), verifySAS]); + const content = e.getContent(); + await this.checkMAC(olmSAS, content, macMethod); + } async doSendVerification() { this.waitingForAccept = true; let startContent; - if (this.startEvent) { startContent = this.channel.completedContentFromEvent(this.startEvent); } else { startContent = await this.sendStart(); - } // we might have switched to a different start event, + } + + // we might have switched to a different start event, // but was we didn't call _waitForEvent there was no // call that could throw yet. So check manually that // we're still on the initiator side - - if (!this.initiatedByMe) { throw new _Base.SwitchStartEventError(this.startEvent); } - let e; - try { - e = await this.waitForEvent("m.key.verification.accept"); + e = await this.waitForEvent(_event.EventType.KeyVerificationAccept); } finally { this.waitingForAccept = false; } - let content = e.getContent(); const sasMethods = intersection(content.short_authentication_string, SAS_SET); - if (!(KEY_AGREEMENT_SET.has(content.key_agreement_protocol) && HASHES_SET.has(content.hash) && MAC_SET.has(content.message_authentication_code) && sasMethods.length)) { throw (0, _Error.newUnknownMethodError)(); } - if (typeof content.commitment !== "string") { throw (0, _Error.newInvalidMessageError)(); } - const keyAgreement = content.key_agreement_protocol; const macMethod = content.message_authentication_code; const hashCommitment = content.commitment; const olmSAS = new global.Olm.SAS(); - try { this.ourSASPubKey = olmSAS.get_pubkey(); - await this.send("m.key.verification.key", { + await this.send(_event.EventType.KeyVerificationKey, { key: this.ourSASPubKey }); - e = await this.waitForEvent("m.key.verification.key"); // FIXME: make sure event is properly formed - + e = await this.waitForEvent(_event.EventType.KeyVerificationKey); + // FIXME: make sure event is properly formed content = e.getContent(); - - const commitmentStr = content.key + _anotherJson.default.stringify(startContent); // TODO: use selected hash function (when we support multiple) - - + const commitmentStr = content.key + _anotherJson.default.stringify(startContent); + // TODO: use selected hash function (when we support multiple) if (olmutil.sha256(commitmentStr) !== hashCommitment) { throw newMismatchedCommitmentError(); } - this.theirSASPubKey = content.key; olmSAS.set_their_key(content.key); - const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); - const verifySAS = new Promise((resolve, reject) => { - this.sasEvent = { - sas: generateSas(sasBytes, sasMethods), - confirm: async () => { - try { - await this.sendMAC(olmSAS, macMethod); - resolve(); - } catch (err) { - reject(err); - } - }, - cancel: () => reject((0, _Error.newUserCancelledError)()), - mismatch: () => reject(newMismatchedSASError()) - }; - this.emit(SasEvent.ShowSas, this.sasEvent); - }); - [e] = await Promise.all([this.waitForEvent("m.key.verification.mac").then(e => { - // we don't expect any more messages from the other - // party, and they may send a m.key.verification.done - // when they're done on their end - this.expectedEvent = "m.key.verification.done"; - return e; - }), verifySAS]); - content = e.getContent(); - await this.checkMAC(olmSAS, content, macMethod); + await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod); } finally { olmSAS.free(); } } - async doRespondVerification() { // as m.related_to is not included in the encrypted content in e2e rooms, // we need to make sure it is added - let content = this.channel.completedContentFromEvent(this.startEvent); // Note: we intersect using our pre-made lists, rather than the sets, + let content = this.channel.completedContentFromEvent(this.startEvent); + + // Note: we intersect using our pre-made lists, rather than the sets, // so that the result will be in our order of preference. Then // fetching the first element from the array will give our preferred // method out of the ones offered by the other party. - const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0]; const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0]; - const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0]; // FIXME: allow app to specify what SAS methods can be used - + const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0]; + // FIXME: allow app to specify what SAS methods can be used const sasMethods = intersection(content.short_authentication_string, SAS_SET); - if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) { throw (0, _Error.newUnknownMethodError)(); } - const olmSAS = new global.Olm.SAS(); - try { const commitmentStr = olmSAS.get_pubkey() + _anotherJson.default.stringify(content); - - await this.send("m.key.verification.accept", { + await this.send(_event.EventType.KeyVerificationAccept, { key_agreement_protocol: keyAgreement, hash: hashMethod, message_authentication_code: macMethod, @@ -378,46 +386,20 @@ // TODO: use selected hash function (when we support multiple) commitment: olmutil.sha256(commitmentStr) }); - let e = await this.waitForEvent("m.key.verification.key"); // FIXME: make sure event is properly formed - + const e = await this.waitForEvent(_event.EventType.KeyVerificationKey); + // FIXME: make sure event is properly formed content = e.getContent(); this.theirSASPubKey = content.key; olmSAS.set_their_key(content.key); this.ourSASPubKey = olmSAS.get_pubkey(); - await this.send("m.key.verification.key", { + await this.send(_event.EventType.KeyVerificationKey, { key: this.ourSASPubKey }); - const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); - const verifySAS = new Promise((resolve, reject) => { - this.sasEvent = { - sas: generateSas(sasBytes, sasMethods), - confirm: async () => { - try { - await this.sendMAC(olmSAS, macMethod); - resolve(); - } catch (err) { - reject(err); - } - }, - cancel: () => reject((0, _Error.newUserCancelledError)()), - mismatch: () => reject(newMismatchedSASError()) - }; - this.emit(SasEvent.ShowSas, this.sasEvent); - }); - [e] = await Promise.all([this.waitForEvent("m.key.verification.mac").then(e => { - // we don't expect any more messages from the other - // party, and they may send a m.key.verification.done - // when they're done on their end - this.expectedEvent = "m.key.verification.done"; - return e; - }), verifySAS]); - content = e.getContent(); - await this.checkMAC(olmSAS, content, macMethod); + await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod); } finally { olmSAS.free(); } } - sendMAC(olmSAS, method) { const mac = {}; const keyList = []; @@ -426,34 +408,27 @@ mac[deviceKeyId] = calculateMAC(olmSAS, method)(this.baseApis.getDeviceEd25519Key(), baseInfo + deviceKeyId); keyList.push(deviceKeyId); const crossSigningId = this.baseApis.getCrossSigningId(); - if (crossSigningId) { const crossSigningKeyId = `ed25519:${crossSigningId}`; mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(crossSigningId, baseInfo + crossSigningKeyId); keyList.push(crossSigningKeyId); } - const keys = calculateMAC(olmSAS, method)(keyList.sort().join(","), baseInfo + "KEY_IDS"); - return this.send("m.key.verification.mac", { + return this.send(_event.EventType.KeyVerificationMac, { mac, keys }); } - async checkMAC(olmSAS, content, method) { const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.userId + this.deviceId + this.baseApis.getUserId() + this.baseApis.deviceId + this.channel.transactionId; - if (content.keys !== calculateMAC(olmSAS, method)(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS")) { throw (0, _Error.newKeyMismatchError)(); } - await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => { if (keyInfo !== calculateMAC(olmSAS, method)(device.keys[keyId], baseInfo + keyId)) { throw (0, _Error.newKeyMismatchError)(); } }); } - } - exports.SAS = SAS; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/embedded.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/embedded.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/embedded.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/embedded.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,247 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RoomWidgetClient = void 0; +var _matrixWidgetApi = require("matrix-widget-api"); +var _event = require("./models/event"); +var _event2 = require("./@types/event"); +var _logger = require("./logger"); +var _client = require("./client"); +var _sync = require("./sync"); +var _slidingSyncSdk = require("./sliding-sync-sdk"); +var _user = require("./models/user"); +var _utils = require("./utils"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/** + * A MatrixClient that routes its requests through the widget API instead of the + * real CS API. + * @experimental This class is considered unstable! + */ +class RoomWidgetClient extends _client.MatrixClient { + constructor(widgetApi, capabilities, roomId, opts) { + super(opts); + + // Request capabilities for the functionality this client needs to support + this.widgetApi = widgetApi; + this.capabilities = capabilities; + this.roomId = roomId; + _defineProperty(this, "room", void 0); + _defineProperty(this, "widgetApiReady", new Promise(resolve => this.widgetApi.once("ready", resolve))); + _defineProperty(this, "lifecycle", void 0); + _defineProperty(this, "syncState", null); + _defineProperty(this, "onEvent", async ev => { + ev.preventDefault(); + + // Verify the room ID matches, since it's possible for the client to + // send us events from other rooms if this widget is always on screen + if (ev.detail.data.room_id === this.roomId) { + const event = new _event.MatrixEvent(ev.detail.data); + await this.syncApi.injectRoomEvents(this.room, [], [event]); + this.emit(_client.ClientEvent.Event, event); + this.setSyncState(_sync.SyncState.Syncing); + _logger.logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); + } else { + const { + event_id: eventId, + room_id: roomId + } = ev.detail.data; + _logger.logger.info(`Received event ${eventId} for a different room ${roomId}; discarding`); + } + await this.ack(ev); + }); + _defineProperty(this, "onToDevice", async ev => { + ev.preventDefault(); + const event = new _event.MatrixEvent({ + type: ev.detail.data.type, + sender: ev.detail.data.sender, + content: ev.detail.data.content + }); + // Mark the event as encrypted if it was, using fake contents and keys since those are unknown to us + if (ev.detail.data.encrypted) event.makeEncrypted(_event2.EventType.RoomMessageEncrypted, {}, "", ""); + this.emit(_client.ClientEvent.ToDeviceEvent, event); + this.setSyncState(_sync.SyncState.Syncing); + await this.ack(ev); + }); + if (capabilities.sendEvent?.length || capabilities.receiveEvent?.length || capabilities.sendMessage === true || Array.isArray(capabilities.sendMessage) && capabilities.sendMessage.length || capabilities.receiveMessage === true || Array.isArray(capabilities.receiveMessage) && capabilities.receiveMessage.length || capabilities.sendState?.length || capabilities.receiveState?.length) { + widgetApi.requestCapabilityForRoomTimeline(roomId); + } + capabilities.sendEvent?.forEach(eventType => widgetApi.requestCapabilityToSendEvent(eventType)); + capabilities.receiveEvent?.forEach(eventType => widgetApi.requestCapabilityToReceiveEvent(eventType)); + if (capabilities.sendMessage === true) { + widgetApi.requestCapabilityToSendMessage(); + } else if (Array.isArray(capabilities.sendMessage)) { + capabilities.sendMessage.forEach(msgType => widgetApi.requestCapabilityToSendMessage(msgType)); + } + if (capabilities.receiveMessage === true) { + widgetApi.requestCapabilityToReceiveMessage(); + } else if (Array.isArray(capabilities.receiveMessage)) { + capabilities.receiveMessage.forEach(msgType => widgetApi.requestCapabilityToReceiveMessage(msgType)); + } + capabilities.sendState?.forEach(({ + eventType, + stateKey + }) => widgetApi.requestCapabilityToSendState(eventType, stateKey)); + capabilities.receiveState?.forEach(({ + eventType, + stateKey + }) => widgetApi.requestCapabilityToReceiveState(eventType, stateKey)); + capabilities.sendToDevice?.forEach(eventType => widgetApi.requestCapabilityToSendToDevice(eventType)); + capabilities.receiveToDevice?.forEach(eventType => widgetApi.requestCapabilityToReceiveToDevice(eventType)); + if (capabilities.turnServers) { + widgetApi.requestCapability(_matrixWidgetApi.MatrixCapabilities.MSC3846TurnServers); + } + widgetApi.on(`action:${_matrixWidgetApi.WidgetApiToWidgetAction.SendEvent}`, this.onEvent); + widgetApi.on(`action:${_matrixWidgetApi.WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); + + // Open communication with the host + widgetApi.start(); + } + async startClient(opts = {}) { + this.lifecycle = new AbortController(); + + // Create our own user object artificially (instead of waiting for sync) + // so it's always available, even if the user is not in any rooms etc. + const userId = this.getUserId(); + if (userId) { + this.store.storeUser(new _user.User(userId)); + } + + // Even though we have no access token and cannot sync, the sync class + // still has some valuable helper methods that we make use of, so we + // instantiate it anyways + if (opts.slidingSync) { + this.syncApi = new _slidingSyncSdk.SlidingSyncSdk(opts.slidingSync, this, opts, this.buildSyncApiOptions()); + } else { + this.syncApi = new _sync.SyncApi(this, opts, this.buildSyncApiOptions()); + } + this.room = this.syncApi.createRoom(this.roomId); + this.store.storeRoom(this.room); + await this.widgetApiReady; + + // Backfill the requested events + // We only get the most recent event for every type + state key combo, + // so it doesn't really matter what order we inject them in + await Promise.all(this.capabilities.receiveState?.map(async ({ + eventType, + stateKey + }) => { + const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]); + const events = rawEvents.map(rawEvent => new _event.MatrixEvent(rawEvent)); + await this.syncApi.injectRoomEvents(this.room, [], events); + events.forEach(event => { + this.emit(_client.ClientEvent.Event, event); + _logger.logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`); + }); + }) ?? []); + this.setSyncState(_sync.SyncState.Syncing); + _logger.logger.info("Finished backfilling events"); + + // Watch for TURN servers, if requested + if (this.capabilities.turnServers) this.watchTurnServers(); + } + stopClient() { + this.widgetApi.off(`action:${_matrixWidgetApi.WidgetApiToWidgetAction.SendEvent}`, this.onEvent); + this.widgetApi.off(`action:${_matrixWidgetApi.WidgetApiToWidgetAction.SendToDevice}`, this.onToDevice); + super.stopClient(); + this.lifecycle.abort(); // Signal to other async tasks that the client has stopped + } + + async joinRoom(roomIdOrAlias) { + if (roomIdOrAlias === this.roomId) return this.room; + throw new Error(`Unknown room: ${roomIdOrAlias}`); + } + async encryptAndSendEvent(room, event) { + let response; + try { + response = await this.widgetApi.sendRoomEvent(event.getType(), event.getContent(), room.roomId); + } catch (e) { + this.updatePendingEventStatus(room, event, _event.EventStatus.NOT_SENT); + throw e; + } + room.updatePendingEvent(event, _event.EventStatus.SENT, response.event_id); + return { + event_id: response.event_id + }; + } + async sendStateEvent(roomId, eventType, content, stateKey = "") { + return await this.widgetApi.sendStateEvent(eventType, stateKey, content, roomId); + } + async sendToDevice(eventType, contentMap) { + await this.widgetApi.sendToDevice(eventType, false, (0, _utils.recursiveMapToObject)(contentMap)); + return {}; + } + async queueToDevice({ + eventType, + batch + }) { + // map: user Id → device Id → payload + const contentMap = new _utils.MapWithDefault(() => new Map()); + for (const { + userId, + deviceId, + payload + } of batch) { + contentMap.getOrCreate(userId).set(deviceId, payload); + } + await this.widgetApi.sendToDevice(eventType, false, (0, _utils.recursiveMapToObject)(contentMap)); + } + async encryptAndSendToDevices(userDeviceInfoArr, payload) { + // map: user Id → device Id → payload + const contentMap = new _utils.MapWithDefault(() => new Map()); + for (const { + userId, + deviceInfo: { + deviceId + } + } of userDeviceInfoArr) { + contentMap.getOrCreate(userId).set(deviceId, payload); + } + await this.widgetApi.sendToDevice(payload.type, true, (0, _utils.recursiveMapToObject)(contentMap)); + } + + // Overridden since we get TURN servers automatically over the widget API, + // and this method would otherwise complain about missing an access token + async checkTurnServers() { + return this.turnServers.length > 0; + } + + // Overridden since we 'sync' manually without the sync API + getSyncState() { + return this.syncState; + } + setSyncState(state) { + const oldState = this.syncState; + this.syncState = state; + this.emit(_client.ClientEvent.Sync, state, oldState); + } + async ack(ev) { + await this.widgetApi.transport.reply(ev.detail, {}); + } + async watchTurnServers() { + const servers = this.widgetApi.getTurnServers(); + const onClientStopped = () => { + servers.return(undefined); + }; + this.lifecycle.signal.addEventListener("abort", onClientStopped); + try { + for await (const server of servers) { + this.turnServers = [{ + urls: server.uris, + username: server.username, + credential: server.password + }]; + this.emit(_client.ClientEvent.TurnServers, this.turnServers); + _logger.logger.log(`Received TURN server: ${server.uris}`); + } + } catch (e) { + _logger.logger.warn("Error watching TURN servers", e); + } finally { + this.lifecycle.signal.removeEventListener("abort", onClientStopped); + } + } +} +exports.RoomWidgetClient = RoomWidgetClient; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/errors.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/errors.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/errors.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/errors.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,58 +3,60 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.InvalidCryptoStoreError = InvalidCryptoStoreError; -exports.InvalidStoreError = InvalidStoreError; -exports.KeySignatureUploadError = void 0; - -// can't just do InvalidStoreError extends Error -// because of http://babeljs.io/docs/usage/caveats/#classes -function InvalidStoreError(reason, value) { - const message = `Store is invalid because ${reason}, ` + `please stop the client, delete all data and start the client again`; - const instance = Reflect.construct(Error, [message]); - Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); - instance.reason = reason; - instance.value = value; - return instance; -} - -InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING"; -InvalidStoreError.prototype = Object.create(Error.prototype, { - constructor: { - value: Error, - enumerable: false, - writable: true, - configurable: true +exports.KeySignatureUploadError = exports.InvalidStoreState = exports.InvalidStoreError = exports.InvalidCryptoStoreState = exports.InvalidCryptoStoreError = void 0; +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +let InvalidStoreState; +exports.InvalidStoreState = InvalidStoreState; +(function (InvalidStoreState) { + InvalidStoreState[InvalidStoreState["ToggledLazyLoading"] = 0] = "ToggledLazyLoading"; +})(InvalidStoreState || (exports.InvalidStoreState = InvalidStoreState = {})); +class InvalidStoreError extends Error { + constructor(reason, value) { + const message = `Store is invalid because ${reason}, ` + `please stop the client, delete all data and start the client again`; + super(message); + this.reason = reason; + this.value = value; + this.name = "InvalidStoreError"; } -}); -Reflect.setPrototypeOf(InvalidStoreError, Error); - -function InvalidCryptoStoreError(reason) { - const message = `Crypto store is invalid because ${reason}, ` + `please stop the client, delete all data and start the client again`; - const instance = Reflect.construct(Error, [message]); - Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); - instance.reason = reason; - instance.name = 'InvalidCryptoStoreError'; - return instance; } - -InvalidCryptoStoreError.TOO_NEW = "TOO_NEW"; -InvalidCryptoStoreError.prototype = Object.create(Error.prototype, { - constructor: { - value: Error, - enumerable: false, - writable: true, - configurable: true +exports.InvalidStoreError = InvalidStoreError; +_defineProperty(InvalidStoreError, "TOGGLED_LAZY_LOADING", InvalidStoreState.ToggledLazyLoading); +let InvalidCryptoStoreState; +exports.InvalidCryptoStoreState = InvalidCryptoStoreState; +(function (InvalidCryptoStoreState) { + InvalidCryptoStoreState["TooNew"] = "TOO_NEW"; +})(InvalidCryptoStoreState || (exports.InvalidCryptoStoreState = InvalidCryptoStoreState = {})); +class InvalidCryptoStoreError extends Error { + constructor(reason) { + const message = `Crypto store is invalid because ${reason}, ` + `please stop the client, delete all data and start the client again`; + super(message); + this.reason = reason; + this.name = "InvalidCryptoStoreError"; } -}); -Reflect.setPrototypeOf(InvalidCryptoStoreError, Error); - +} +exports.InvalidCryptoStoreError = InvalidCryptoStoreError; +_defineProperty(InvalidCryptoStoreError, "TOO_NEW", InvalidCryptoStoreState.TooNew); class KeySignatureUploadError extends Error { constructor(message, value) { super(message); this.value = value; } - } - exports.KeySignatureUploadError = KeySignatureUploadError; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/event-mapper.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/event-mapper.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/event-mapper.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/event-mapper.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,64 +4,69 @@ value: true }); exports.eventMapperFor = eventMapperFor; - var _event = require("./models/event"); - +var _event2 = require("./@types/event"); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function eventMapperFor(client, options) { let preventReEmit = Boolean(options.preventReEmit); const decrypt = options.decrypt !== false; - function mapper(plainOldJsObject) { if (options.toDevice) { delete plainOldJsObject.room_id; } - const room = client.getRoom(plainOldJsObject.room_id); - let event; // If the event is already known to the room, let's re-use the model rather than duplicating. + let event; + // If the event is already known to the room, let's re-use the model rather than duplicating. // We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour. - if (room && plainOldJsObject.state_key === undefined) { event = room.findEventById(plainOldJsObject.event_id); } - if (!event || event.status) { event = new _event.MatrixEvent(plainOldJsObject); } else { // merge the latest unsigned data from the server - event.setUnsigned(_objectSpread(_objectSpread({}, event.getUnsigned()), plainOldJsObject.unsigned)); // prevent doubling up re-emitters - + event.setUnsigned(_objectSpread(_objectSpread({}, event.getUnsigned()), plainOldJsObject.unsigned)); + // prevent doubling up re-emitters preventReEmit = true; } + // if there is a complete edit bundled alongside the event, perform the replacement. + // (prior to MSC3925, events were automatically replaced on the server-side. MSC3925 proposes that that doesn't + // happen automatically but the server does provide us with the whole content of the edit event.) + const bundledEdit = event.getServerAggregatedRelation(_event2.RelationType.Replace); + if (bundledEdit?.content) { + const replacement = mapper(bundledEdit); + // XXX: it's worth noting that the spec says we should only respect encrypted edits if, once decrypted, the + // replacement has a `m.new_content` property. The problem is that we haven't yet decrypted the replacement + // (it should be happening in the background), so we can't enforce this. Possibly we should for decryption + // to complete, but that sounds a bit racy. For now, we just assume it's ok. + event.makeReplaced(replacement); + } const thread = room?.findThreadForEvent(event); - if (thread) { event.setThread(thread); } + // TODO: once we get rid of the old libolm-backed crypto, we can restrict this to room events (rather than + // to-device events), because the rust implementation decrypts to-device messages at a higher level. + // Generally we probably want to use a different eventMapper implementation for to-device events because if (event.isEncrypted()) { if (!preventReEmit) { client.reEmitter.reEmit(event, [_event.MatrixEventEvent.Decrypted]); } - if (decrypt) { client.decryptEventIfNeeded(event); } } - if (!preventReEmit) { client.reEmitter.reEmit(event, [_event.MatrixEventEvent.Replaced, _event.MatrixEventEvent.VisibilityChange]); room?.reEmitter.reEmit(event, [_event.MatrixEventEvent.BeforeRedaction]); } - return event; } - return mapper; } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/ExtensibleEvent.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/ExtensibleEvent.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/ExtensibleEvent.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/ExtensibleEvent.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,44 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ExtensibleEvent = void 0; +/* +Copyright 2021 - 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Represents an Extensible Event in Matrix. + */ +class ExtensibleEvent { + constructor(wireFormat) { + this.wireFormat = wireFormat; + } + + /** + * Shortcut to wireFormat.content + */ + get wireContent() { + return this.wireFormat.content; + } + + /** + * Serializes the event into a format which can be used to send the + * event to the room. + * @returns The serialized event. + */ +} +exports.ExtensibleEvent = ExtensibleEvent; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/InvalidEventError.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/InvalidEventError.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/InvalidEventError.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/InvalidEventError.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,31 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.InvalidEventError = void 0; +/* +Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Thrown when an event is unforgivably unparsable. + */ +class InvalidEventError extends Error { + constructor(message) { + super(message); + } +} +exports.InvalidEventError = InvalidEventError; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/MessageEvent.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/MessageEvent.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/MessageEvent.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/MessageEvent.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,127 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MessageEvent = void 0; +var _ExtensibleEvent = require("./ExtensibleEvent"); +var _extensible_events = require("../@types/extensible_events"); +var _utilities = require("./utilities"); +var _InvalidEventError = require("./InvalidEventError"); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/** + * Represents a message event. Message events are the simplest form of event with + * just text (optionally of different mimetypes, like HTML). + * + * Message events can additionally be an Emote or Notice, though typically those + * are represented as EmoteEvent and NoticeEvent respectively. + */ +class MessageEvent extends _ExtensibleEvent.ExtensibleEvent { + /** + * The default text for the event. + */ + + /** + * The default HTML for the event, if provided. + */ + + /** + * All the different renderings of the message. Note that this is the same + * format as an m.message body but may contain elements not found directly + * in the event content: this is because this is interpreted based off the + * other information available in the event. + */ + + /** + * Creates a new MessageEvent from a pure format. Note that the event is + * *not* parsed here: it will be treated as a literal m.message primary + * typed event. + * @param wireFormat - The event. + */ + constructor(wireFormat) { + super(wireFormat); + _defineProperty(this, "text", void 0); + _defineProperty(this, "html", void 0); + _defineProperty(this, "renderings", void 0); + const mmessage = _extensible_events.M_MESSAGE.findIn(this.wireContent); + const mtext = _extensible_events.M_TEXT.findIn(this.wireContent); + const mhtml = _extensible_events.M_HTML.findIn(this.wireContent); + if ((0, _utilities.isProvided)(mmessage)) { + if (!Array.isArray(mmessage)) { + throw new _InvalidEventError.InvalidEventError("m.message contents must be an array"); + } + const text = mmessage.find(r => !(0, _utilities.isProvided)(r.mimetype) || r.mimetype === "text/plain"); + const html = mmessage.find(r => r.mimetype === "text/html"); + if (!text) throw new _InvalidEventError.InvalidEventError("m.message is missing a plain text representation"); + this.text = text.body; + this.html = html?.body; + this.renderings = mmessage; + } else if ((0, _utilities.isOptionalAString)(mtext)) { + this.text = mtext; + this.html = mhtml; + this.renderings = [{ + body: mtext, + mimetype: "text/plain" + }]; + if (this.html) { + this.renderings.push({ + body: this.html, + mimetype: "text/html" + }); + } + } else { + throw new _InvalidEventError.InvalidEventError("Missing textual representation for event"); + } + } + isEquivalentTo(primaryEventType) { + return (0, _extensible_events.isEventTypeSame)(primaryEventType, _extensible_events.M_MESSAGE); + } + serializeMMessageOnly() { + let messageRendering = { + [_extensible_events.M_MESSAGE.name]: this.renderings + }; + + // Use the shorthand if it's just a simple text event + if (this.renderings.length === 1) { + const mime = this.renderings[0].mimetype; + if (mime === undefined || mime === "text/plain") { + messageRendering = { + [_extensible_events.M_TEXT.name]: this.renderings[0].body + }; + } + } + return messageRendering; + } + serialize() { + return { + type: "m.room.message", + content: _objectSpread(_objectSpread({}, this.serializeMMessageOnly()), {}, { + body: this.text, + msgtype: "m.text", + format: this.html ? "org.matrix.custom.html" : undefined, + formatted_body: this.html ?? undefined + }) + }; + } + + /** + * Creates a new MessageEvent from text and HTML. + * @param text - The text. + * @param html - Optional HTML. + * @returns The representative message event. + */ + static from(text, html) { + return new MessageEvent({ + type: _extensible_events.M_MESSAGE.name, + content: { + [_extensible_events.M_TEXT.name]: text, + [_extensible_events.M_HTML.name]: html + } + }); + } +} +exports.MessageEvent = MessageEvent; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollEndEvent.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollEndEvent.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollEndEvent.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollEndEvent.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,81 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PollEndEvent = void 0; +var _extensible_events = require("../@types/extensible_events"); +var _polls = require("../@types/polls"); +var _ExtensibleEvent = require("./ExtensibleEvent"); +var _InvalidEventError = require("./InvalidEventError"); +var _MessageEvent = require("./MessageEvent"); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/** + * Represents a poll end/closure event. + */ +class PollEndEvent extends _ExtensibleEvent.ExtensibleEvent { + /** + * The poll start event ID referenced by the response. + */ + + /** + * The closing message for the event. + */ + + /** + * Creates a new PollEndEvent from a pure format. Note that the event is *not* + * parsed here: it will be treated as a literal m.poll.response primary typed event. + * @param wireFormat - The event. + */ + constructor(wireFormat) { + super(wireFormat); + _defineProperty(this, "pollEventId", void 0); + _defineProperty(this, "closingMessage", void 0); + const rel = this.wireContent["m.relates_to"]; + if (!_extensible_events.REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel?.event_id !== "string") { + throw new _InvalidEventError.InvalidEventError("Relationship must be a reference to an event"); + } + this.pollEventId = rel.event_id; + this.closingMessage = new _MessageEvent.MessageEvent(this.wireFormat); + } + isEquivalentTo(primaryEventType) { + return (0, _extensible_events.isEventTypeSame)(primaryEventType, _polls.M_POLL_END); + } + serialize() { + return { + type: _polls.M_POLL_END.name, + content: _objectSpread({ + "m.relates_to": { + rel_type: _extensible_events.REFERENCE_RELATION.name, + event_id: this.pollEventId + }, + [_polls.M_POLL_END.name]: {} + }, this.closingMessage.serialize().content) + }; + } + + /** + * Creates a new PollEndEvent from a poll event ID. + * @param pollEventId - The poll start event ID. + * @param message - A closing message, typically revealing the top answer. + * @returns The representative poll closure event. + */ + static from(pollEventId, message) { + return new PollEndEvent({ + type: _polls.M_POLL_END.name, + content: { + "m.relates_to": { + rel_type: _extensible_events.REFERENCE_RELATION.name, + event_id: pollEventId + }, + [_polls.M_POLL_END.name]: {}, + [_extensible_events.M_TEXT.name]: message + } + }); + } +} +exports.PollEndEvent = PollEndEvent; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollResponseEvent.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollResponseEvent.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollResponseEvent.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollResponseEvent.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,126 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PollResponseEvent = void 0; +var _ExtensibleEvent = require("./ExtensibleEvent"); +var _polls = require("../@types/polls"); +var _extensible_events = require("../@types/extensible_events"); +var _InvalidEventError = require("./InvalidEventError"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/** + * Represents a poll response event. + */ +class PollResponseEvent extends _ExtensibleEvent.ExtensibleEvent { + /** + * The provided answers for the poll. Note that this may be falsy/unpredictable if + * the `spoiled` property is true. + */ + get answerIds() { + return this.internalAnswerIds; + } + + /** + * The poll start event ID referenced by the response. + */ + + /** + * Whether the vote is spoiled. + */ + get spoiled() { + return this.internalSpoiled; + } + + /** + * Creates a new PollResponseEvent from a pure format. Note that the event is *not* + * parsed here: it will be treated as a literal m.poll.response primary typed event. + * + * To validate the response against a poll, call `validateAgainst` after creation. + * @param wireFormat - The event. + */ + constructor(wireFormat) { + super(wireFormat); + _defineProperty(this, "internalAnswerIds", []); + _defineProperty(this, "internalSpoiled", false); + _defineProperty(this, "pollEventId", void 0); + const rel = this.wireContent["m.relates_to"]; + if (!_extensible_events.REFERENCE_RELATION.matches(rel?.rel_type) || typeof rel?.event_id !== "string") { + throw new _InvalidEventError.InvalidEventError("Relationship must be a reference to an event"); + } + this.pollEventId = rel.event_id; + this.validateAgainst(null); + } + + /** + * Validates the poll response using the poll start event as a frame of reference. This + * is used to determine if the vote is spoiled, whether the answers are valid, etc. + * @param poll - The poll start event. + */ + validateAgainst(poll) { + const response = _polls.M_POLL_RESPONSE.findIn(this.wireContent); + if (!Array.isArray(response?.answers)) { + this.internalSpoiled = true; + this.internalAnswerIds = []; + return; + } + let answers = response?.answers ?? []; + if (answers.some(a => typeof a !== "string") || answers.length === 0) { + this.internalSpoiled = true; + this.internalAnswerIds = []; + return; + } + if (poll) { + if (answers.some(a => !poll.answers.some(pa => pa.id === a))) { + this.internalSpoiled = true; + this.internalAnswerIds = []; + return; + } + answers = answers.slice(0, poll.maxSelections); + } + this.internalAnswerIds = answers; + this.internalSpoiled = false; + } + isEquivalentTo(primaryEventType) { + return (0, _extensible_events.isEventTypeSame)(primaryEventType, _polls.M_POLL_RESPONSE); + } + serialize() { + return { + type: _polls.M_POLL_RESPONSE.name, + content: { + "m.relates_to": { + rel_type: _extensible_events.REFERENCE_RELATION.name, + event_id: this.pollEventId + }, + [_polls.M_POLL_RESPONSE.name]: { + answers: this.spoiled ? undefined : this.answerIds + } + } + }; + } + + /** + * Creates a new PollResponseEvent from a set of answers. To spoil the vote, pass an empty + * answers array. + * @param answers - The user's answers. Should be valid from a poll's answer IDs. + * @param pollEventId - The poll start event ID. + * @returns The representative poll response event. + */ + static from(answers, pollEventId) { + return new PollResponseEvent({ + type: _polls.M_POLL_RESPONSE.name, + content: { + "m.relates_to": { + rel_type: _extensible_events.REFERENCE_RELATION.name, + event_id: pollEventId + }, + [_polls.M_POLL_RESPONSE.name]: { + answers: answers + } + } + }); + } +} +exports.PollResponseEvent = PollResponseEvent; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollStartEvent.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollStartEvent.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollStartEvent.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/PollStartEvent.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,183 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PollStartEvent = exports.PollAnswerSubevent = void 0; +var _matrixEventsSdk = require("matrix-events-sdk"); +var _MessageEvent = require("./MessageEvent"); +var _extensible_events = require("../@types/extensible_events"); +var _polls = require("../@types/polls"); +var _InvalidEventError = require("./InvalidEventError"); +var _ExtensibleEvent = require("./ExtensibleEvent"); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/** + * Represents a poll answer. Note that this is represented as a subtype and is + * not registered as a parsable event - it is implied for usage exclusively + * within the PollStartEvent parsing. + */ +class PollAnswerSubevent extends _MessageEvent.MessageEvent { + /** + * The answer ID. + */ + + constructor(wireFormat) { + super(wireFormat); + _defineProperty(this, "id", void 0); + const id = wireFormat.content.id; + if (!id || typeof id !== "string") { + throw new _InvalidEventError.InvalidEventError("Answer ID must be a non-empty string"); + } + this.id = id; + } + serialize() { + return { + type: "org.matrix.sdk.poll.answer", + content: _objectSpread({ + id: this.id + }, this.serializeMMessageOnly()) + }; + } + + /** + * Creates a new PollAnswerSubevent from ID and text. + * @param id - The answer ID (unique within the poll). + * @param text - The text. + * @returns The representative answer. + */ + static from(id, text) { + return new PollAnswerSubevent({ + type: "org.matrix.sdk.poll.answer", + content: { + id: id, + [_extensible_events.M_TEXT.name]: text + } + }); + } +} + +/** + * Represents a poll start event. + */ +exports.PollAnswerSubevent = PollAnswerSubevent; +class PollStartEvent extends _ExtensibleEvent.ExtensibleEvent { + /** + * The question being asked, as a MessageEvent node. + */ + + /** + * The interpreted kind of poll. Note that this will infer a value that is known to the + * SDK rather than verbatim - this means unknown types will be represented as undisclosed + * polls. + * + * To get the raw kind, use rawKind. + */ + + /** + * The true kind as provided by the event sender. Might not be valid. + */ + + /** + * The maximum number of selections a user is allowed to make. + */ + + /** + * The possible answers for the poll. + */ + + /** + * Creates a new PollStartEvent from a pure format. Note that the event is *not* + * parsed here: it will be treated as a literal m.poll.start primary typed event. + * @param wireFormat - The event. + */ + constructor(wireFormat) { + super(wireFormat); + _defineProperty(this, "question", void 0); + _defineProperty(this, "kind", void 0); + _defineProperty(this, "rawKind", void 0); + _defineProperty(this, "maxSelections", void 0); + _defineProperty(this, "answers", void 0); + const poll = _polls.M_POLL_START.findIn(this.wireContent); + if (!poll?.question) { + throw new _InvalidEventError.InvalidEventError("A question is required"); + } + this.question = new _MessageEvent.MessageEvent({ + type: "org.matrix.sdk.poll.question", + content: poll.question + }); + this.rawKind = poll.kind; + if (_polls.M_POLL_KIND_DISCLOSED.matches(this.rawKind)) { + this.kind = _polls.M_POLL_KIND_DISCLOSED; + } else { + this.kind = _polls.M_POLL_KIND_UNDISCLOSED; // default & assumed value + } + + this.maxSelections = Number.isFinite(poll.max_selections) && poll.max_selections > 0 ? poll.max_selections : 1; + if (!Array.isArray(poll.answers)) { + throw new _InvalidEventError.InvalidEventError("Poll answers must be an array"); + } + const answers = poll.answers.slice(0, 20).map(a => new PollAnswerSubevent({ + type: "org.matrix.sdk.poll.answer", + content: a + })); + if (answers.length <= 0) { + throw new _InvalidEventError.InvalidEventError("No answers available"); + } + this.answers = answers; + } + isEquivalentTo(primaryEventType) { + return (0, _extensible_events.isEventTypeSame)(primaryEventType, _polls.M_POLL_START); + } + serialize() { + return { + type: _polls.M_POLL_START.name, + content: { + [_polls.M_POLL_START.name]: { + question: this.question.serialize().content, + kind: this.rawKind, + max_selections: this.maxSelections, + answers: this.answers.map(a => a.serialize().content) + }, + [_extensible_events.M_TEXT.name]: `${this.question.text}\n${this.answers.map((a, i) => `${i + 1}. ${a.text}`).join("\n")}` + } + }; + } + + /** + * Creates a new PollStartEvent from question, answers, and metadata. + * @param question - The question to ask. + * @param answers - The answers. Should be unique within each other. + * @param kind - The kind of poll. + * @param maxSelections - The maximum number of selections. Must be 1 or higher. + * @returns The representative poll start event. + */ + static from(question, answers, kind, maxSelections = 1) { + return new PollStartEvent({ + type: _polls.M_POLL_START.name, + content: { + [_extensible_events.M_TEXT.name]: question, + // unused by parsing + [_polls.M_POLL_START.name]: { + question: { + [_extensible_events.M_TEXT.name]: question + }, + kind: kind instanceof _matrixEventsSdk.NamespacedValue ? kind.name : kind, + max_selections: maxSelections, + answers: answers.map(a => ({ + id: makeId(), + [_extensible_events.M_TEXT.name]: a + })) + } + } + }); + } +} +exports.PollStartEvent = PollStartEvent; +const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +function makeId() { + return [...Array(16)].map(() => LETTERS.charAt(Math.floor(Math.random() * LETTERS.length))).join(""); +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/utilities.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/utilities.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/utilities.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/extensible_events_v1/utilities.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,40 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.isOptionalAString = isOptionalAString; +exports.isProvided = isProvided; +/* +Copyright 2021 - 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Determines if the given optional was provided a value. + * @param s - The optional to test. + * @returns True if the value is defined. + */ +function isProvided(s) { + return s !== null && s !== undefined; +} + +/** + * Determines if the given optional string is a defined string. + * @param s - The input string. + * @returns True if the input is a defined string. + */ +function isOptionalAString(s) { + return isProvided(s) && typeof s === "string"; +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/feature.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/feature.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/feature.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/feature.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,74 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ServerSupport = exports.Feature = void 0; +exports.buildFeatureSupportMap = buildFeatureSupportMap; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +let ServerSupport; +exports.ServerSupport = ServerSupport; +(function (ServerSupport) { + ServerSupport[ServerSupport["Stable"] = 0] = "Stable"; + ServerSupport[ServerSupport["Unstable"] = 1] = "Unstable"; + ServerSupport[ServerSupport["Unsupported"] = 2] = "Unsupported"; +})(ServerSupport || (exports.ServerSupport = ServerSupport = {})); +let Feature; +exports.Feature = Feature; +(function (Feature) { + Feature["Thread"] = "Thread"; + Feature["ThreadUnreadNotifications"] = "ThreadUnreadNotifications"; + Feature["LoginTokenRequest"] = "LoginTokenRequest"; + Feature["RelationBasedRedactions"] = "RelationBasedRedactions"; + Feature["AccountDataDeletion"] = "AccountDataDeletion"; +})(Feature || (exports.Feature = Feature = {})); +const featureSupportResolver = { + [Feature.Thread]: { + unstablePrefixes: ["org.matrix.msc3440"], + matrixVersion: "v1.3" + }, + [Feature.ThreadUnreadNotifications]: { + unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"], + matrixVersion: "v1.4" + }, + [Feature.LoginTokenRequest]: { + unstablePrefixes: ["org.matrix.msc3882"] + }, + [Feature.RelationBasedRedactions]: { + unstablePrefixes: ["org.matrix.msc3912"] + }, + [Feature.AccountDataDeletion]: { + unstablePrefixes: ["org.matrix.msc3391"] + } +}; +async function buildFeatureSupportMap(versions) { + const supportMap = new Map(); + for (const [feature, supportCondition] of Object.entries(featureSupportResolver)) { + const supportMatrixVersion = versions.versions?.includes(supportCondition.matrixVersion || "") ?? false; + const supportUnstablePrefixes = supportCondition.unstablePrefixes?.every(unstablePrefix => { + return versions.unstable_features?.[unstablePrefix] === true; + }) ?? false; + if (supportMatrixVersion) { + supportMap.set(feature, ServerSupport.Stable); + } else if (supportUnstablePrefixes) { + supportMap.set(feature, ServerSupport.Unstable); + } else { + supportMap.set(feature, ServerSupport.Unsupported); + } + } + return supportMap; +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/filter-component.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/filter-component.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/filter-component.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/filter-component.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,7 @@ value: true }); exports.FilterComponent = void 0; - var _thread = require("./models/thread"); - /* Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. @@ -24,15 +22,11 @@ */ /** - * @module filter-component - */ - -/** * Checks if a value matches a given field value, which may be a * terminated * wildcard pattern. - * @param {String} actualValue The value to be compared - * @param {String} filterValue The filter pattern to be compared - * @return {boolean} true if the actualValue matches the filterValue + * @param actualValue - The value to be compared + * @param filterValue - The filter pattern to be compared + * @returns true if the actualValue matches the filterValue */ function matchesWildcard(actualValue, filterValue) { if (filterValue.endsWith("*")) { @@ -42,8 +36,8 @@ return actualValue === filterValue; } } -/* eslint-disable camelcase */ +/* eslint-disable camelcase */ /* eslint-enable camelcase */ @@ -54,149 +48,124 @@ * * N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as * 'Filters' are referred to as 'FilterCollections'. - * - * @constructor - * @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true } */ class FilterComponent { constructor(filterJson, userId) { this.filterJson = filterJson; this.userId = userId; } + /** * Checks with the filter component matches the given event - * @param {MatrixEvent} event event to be checked against the filter - * @return {boolean} true if the event matches the filter + * @param event - event to be checked against the filter + * @returns true if the event matches the filter */ - - check(event) { const bundledRelationships = event.getUnsigned()?.["m.relations"] || {}; - const relations = Object.keys(bundledRelationships); // Relation senders allows in theory a look-up of any senders + const relations = Object.keys(bundledRelationships); + // Relation senders allows in theory a look-up of any senders // however clients can only know about the current user participation status // as sending a whole list of participants could be proven problematic in terms // of performance // This should be improved when bundled relationships solve that problem - const relationSenders = []; - if (this.userId && bundledRelationships?.[_thread.THREAD_RELATION_TYPE.name]?.current_user_participated) { relationSenders.push(this.userId); } - return this.checkFields(event.getRoomId(), event.getSender(), event.getType(), event.getContent() ? event.getContent().url !== undefined : false, relations, relationSenders); } + /** * Converts the filter component into the form expected over the wire */ - - toJSON() { return { - "types": this.filterJson.types || null, - "not_types": this.filterJson.not_types || [], - "rooms": this.filterJson.rooms || null, - "not_rooms": this.filterJson.not_rooms || [], - "senders": this.filterJson.senders || null, - "not_senders": this.filterJson.not_senders || [], - "contains_url": this.filterJson.contains_url || null, + types: this.filterJson.types || null, + not_types: this.filterJson.not_types || [], + rooms: this.filterJson.rooms || null, + not_rooms: this.filterJson.not_rooms || [], + senders: this.filterJson.senders || null, + not_senders: this.filterJson.not_senders || [], + contains_url: this.filterJson.contains_url || null, [_thread.FILTER_RELATED_BY_SENDERS.name]: this.filterJson[_thread.FILTER_RELATED_BY_SENDERS.name] || [], [_thread.FILTER_RELATED_BY_REL_TYPES.name]: this.filterJson[_thread.FILTER_RELATED_BY_REL_TYPES.name] || [] }; } + /** * Checks whether the filter component matches the given event fields. - * @param {String} roomId the roomId for the event being checked - * @param {String} sender the sender of the event being checked - * @param {String} eventType the type of the event being checked - * @param {boolean} containsUrl whether the event contains a content.url field - * @param {boolean} relationTypes whether has aggregated relation of the given type - * @param {boolean} relationSenders whether one of the relation is sent by the user listed - * @return {boolean} true if the event fields match the filter + * @param roomId - the roomId for the event being checked + * @param sender - the sender of the event being checked + * @param eventType - the type of the event being checked + * @param containsUrl - whether the event contains a content.url field + * @param relationTypes - whether has aggregated relation of the given type + * @param relationSenders - whether one of the relation is sent by the user listed + * @returns true if the event fields match the filter */ - - checkFields(roomId, sender, eventType, containsUrl, relationTypes, relationSenders) { const literalKeys = { - "rooms": function (v) { + rooms: function (v) { return roomId === v; }, - "senders": function (v) { + senders: function (v) { return sender === v; }, - "types": function (v) { + types: function (v) { return matchesWildcard(eventType, v); } }; - - for (let n = 0; n < Object.keys(literalKeys).length; n++) { - const name = Object.keys(literalKeys)[n]; + for (const name in literalKeys) { const matchFunc = literalKeys[name]; const notName = "not_" + name; const disallowedValues = this.filterJson[notName]; - if (disallowedValues?.some(matchFunc)) { return false; } - const allowedValues = this.filterJson[name]; - if (allowedValues && !allowedValues.some(matchFunc)) { return false; } } - const containsUrlFilter = this.filterJson.contains_url; - if (containsUrlFilter !== undefined && containsUrlFilter !== containsUrl) { return false; } - const relationTypesFilter = this.filterJson[_thread.FILTER_RELATED_BY_REL_TYPES.name]; - if (relationTypesFilter !== undefined) { if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) { return false; } } - const relationSendersFilter = this.filterJson[_thread.FILTER_RELATED_BY_SENDERS.name]; - if (relationSendersFilter !== undefined) { if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) { return false; } } - return true; } - arrayMatchesFilter(filter, values) { return values.length > 0 && filter.every(value => { return values.includes(value); }); } + /** * Filters a list of events down to those which match this filter component - * @param {MatrixEvent[]} events Events to be checked against the filter component - * @return {MatrixEvent[]} events which matched the filter component + * @param events - Events to be checked against the filter component + * @returns events which matched the filter component */ - - filter(events) { return events.filter(this.check, this); } + /** * Returns the limit field for a given filter component, providing a default of * 10 if none is otherwise specified. Cargo-culted from Synapse. - * @return {Number} the limit for this filter component. + * @returns the limit for this filter component. */ - - limit() { return this.filterJson.limit !== undefined ? this.filterJson.limit : 10; } - } - exports.FilterComponent = FilterComponent; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/filter.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/filter.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/filter.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/filter.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,94 +4,78 @@ value: true }); exports.Filter = void 0; - +var _sync = require("./@types/sync"); var _filterComponent = require("./filter-component"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** - * @param {Object} obj - * @param {string} keyNesting - * @param {*} val */ function setProp(obj, keyNesting, val) { const nestedKeys = keyNesting.split("."); let currentObj = obj; - for (let i = 0; i < nestedKeys.length - 1; i++) { if (!currentObj[nestedKeys[i]]) { currentObj[nestedKeys[i]] = {}; } - currentObj = currentObj[nestedKeys[i]]; } - currentObj[nestedKeys[nestedKeys.length - 1]] = val; } -/* eslint-disable camelcase */ +/* eslint-disable camelcase */ /* eslint-enable camelcase */ -/** - * Construct a new Filter. - * @constructor - * @param {string} userId The user ID for this filter. - * @param {string=} filterId The filter ID if known. - * @prop {string} userId The user ID of the filter - * @prop {?string} filterId The filter ID - */ class Filter { /** * Create a filter from existing data. - * @static - * @param {string} userId - * @param {string} filterId - * @param {Object} jsonObj - * @return {Filter} */ static fromJson(userId, filterId, jsonObj) { const filter = new Filter(userId, filterId); filter.setDefinition(jsonObj); return filter; } - + /** + * Construct a new Filter. + * @param userId - The user ID for this filter. + * @param filterId - The filter ID if known. + */ constructor(userId, filterId) { this.userId = userId; this.filterId = filterId; - _defineProperty(this, "definition", {}); - _defineProperty(this, "roomFilter", void 0); - _defineProperty(this, "roomTimelineFilter", void 0); } + /** * Get the ID of this filter on your homeserver (if known) - * @return {?string} The filter ID + * @returns The filter ID */ - - getFilterId() { return this.filterId; } + /** * Get the JSON body of the filter. - * @return {Object} The filter definition + * @returns The filter definition */ - - getDefinition() { return this.definition; } + /** * Set the JSON body of the filter - * @param {Object} definition The filter definition + * @param definition - The filter definition */ + setDefinition(definition) { + this.definition = definition; + // This is all ported from synapse's FilterCollection() - setDefinition(definition) { - this.definition = definition; // This is all ported from synapse's FilterCollection() // definitions look something like: // { // "room": { @@ -123,22 +107,22 @@ // "event_fields": ["type", "content", "sender"] // } - const roomFilterJson = definition.room; // consider the top level rooms/not_rooms filter + const roomFilterJson = definition.room; + // consider the top level rooms/not_rooms filter const roomFilterFields = {}; - if (roomFilterJson) { if (roomFilterJson.rooms) { roomFilterFields.rooms = roomFilterJson.rooms; } - if (roomFilterJson.rooms) { roomFilterFields.not_rooms = roomFilterJson.not_rooms; } } - this.roomFilter = new _filterComponent.FilterComponent(roomFilterFields, this.userId); - this.roomTimelineFilter = new _filterComponent.FilterComponent(roomFilterJson?.timeline || {}, this.userId); // don't bother porting this from synapse yet: + this.roomTimelineFilter = new _filterComponent.FilterComponent(roomFilterJson?.timeline || {}, this.userId); + + // don't bother porting this from synapse yet: // this._room_state_filter = // new FilterComponent(roomFilterJson.state || {}); // this._room_ephemeral_filter = @@ -150,54 +134,65 @@ // this._account_data_filter = // new FilterComponent(definition.account_data || {}); } + /** * Get the room.timeline filter component of the filter - * @return {FilterComponent} room timeline filter component + * @returns room timeline filter component */ - - getRoomTimelineFilterComponent() { return this.roomTimelineFilter; } + /** * Filter the list of events based on whether they are allowed in a timeline * based on this filter - * @param {MatrixEvent[]} events the list of events being filtered - * @return {MatrixEvent[]} the list of events which match the filter + * @param events - the list of events being filtered + * @returns the list of events which match the filter */ - - filterRoomTimeline(events) { - return this.roomTimelineFilter.filter(this.roomFilter.filter(events)); + if (this.roomFilter) { + events = this.roomFilter.filter(events); + } + if (this.roomTimelineFilter) { + events = this.roomTimelineFilter.filter(events); + } + return events; } + /** * Set the max number of events to return for each room's timeline. - * @param {Number} limit The max number of events to return for each room. + * @param limit - The max number of events to return for each room. */ - - setTimelineLimit(limit) { setProp(this.definition, "room.timeline.limit", limit); } + /** + * Enable threads unread notification + */ + setUnreadThreadNotifications(enabled) { + this.definition = _objectSpread(_objectSpread({}, this.definition), {}, { + room: _objectSpread(_objectSpread({}, this.definition?.room), {}, { + timeline: _objectSpread(_objectSpread({}, this.definition?.room?.timeline), {}, { + [_sync.UNREAD_THREAD_NOTIFICATIONS.name]: enabled + }) + }) + }); + } setLazyLoadMembers(enabled) { - setProp(this.definition, "room.state.lazy_load_members", !!enabled); + setProp(this.definition, "room.state.lazy_load_members", enabled); } + /** * Control whether left rooms should be included in responses. - * @param {boolean} includeLeave True to make rooms the user has left appear + * @param includeLeave - True to make rooms the user has left appear * in responses. */ - - setIncludeLeaveRooms(includeLeave) { setProp(this.definition, "room.include_leave", includeLeave); } - } - exports.Filter = Filter; - _defineProperty(Filter, "LAZY_LOADING_MESSAGES_FILTER", { lazy_load_members: true }); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/errors.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/errors.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/errors.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/errors.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,85 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MatrixError = exports.HTTPError = exports.ConnectionError = void 0; +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Construct a generic HTTP error. This is a JavaScript Error with additional information + * specific to HTTP responses. + * @param msg - The error message to include. + * @param httpStatus - The HTTP response status code. + */ +class HTTPError extends Error { + constructor(msg, httpStatus) { + super(msg); + this.httpStatus = httpStatus; + } +} +exports.HTTPError = HTTPError; +class MatrixError extends HTTPError { + // The Matrix 'errcode' value, e.g. "M_FORBIDDEN". + + // The raw Matrix error JSON used to construct this object. + + /** + * Construct a Matrix error. This is a JavaScript Error with additional + * information specific to the standard Matrix error response. + * @param errorJson - The Matrix error JSON returned from the homeserver. + * @param httpStatus - The numeric HTTP status code given + */ + constructor(errorJson = {}, httpStatus, url, event) { + let message = errorJson.error || "Unknown message"; + if (httpStatus) { + message = `[${httpStatus}] ${message}`; + } + if (url) { + message = `${message} (${url})`; + } + super(`MatrixError: ${message}`, httpStatus); + this.httpStatus = httpStatus; + this.url = url; + this.event = event; + _defineProperty(this, "errcode", void 0); + _defineProperty(this, "data", void 0); + this.errcode = errorJson.errcode; + this.name = errorJson.errcode || "Unknown error code"; + this.data = errorJson; + } +} + +/** + * Construct a ConnectionError. This is a JavaScript Error indicating + * that a request failed because of some error with the connection, either + * CORS was not correctly configured on the server, the server didn't response, + * the request timed out, or the internet connection on the client side went down. + */ +exports.MatrixError = MatrixError; +class ConnectionError extends Error { + constructor(message, cause) { + super(message + (cause ? `: ${cause.message}` : "")); + } + get name() { + return "ConnectionError"; + } +} +exports.ConnectionError = ConnectionError; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/fetch.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/fetch.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/fetch.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/fetch.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,249 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.FetchHttpApi = void 0; +var utils = _interopRequireWildcard(require("../utils")); +var _method = require("./method"); +var _errors = require("./errors"); +var _interface = require("./interface"); +var _utils2 = require("./utils"); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +class FetchHttpApi { + constructor(eventEmitter, opts) { + this.eventEmitter = eventEmitter; + this.opts = opts; + _defineProperty(this, "abortController", new AbortController()); + utils.checkObjectHasKeys(opts, ["baseUrl", "prefix"]); + opts.onlyData = !!opts.onlyData; + opts.useAuthorizationHeader = opts.useAuthorizationHeader ?? true; + } + abort() { + this.abortController.abort(); + this.abortController = new AbortController(); + } + fetch(resource, options) { + if (this.opts.fetchFn) { + return this.opts.fetchFn(resource, options); + } + return global.fetch(resource, options); + } + + /** + * Sets the base URL for the identity server + * @param url - The new base url + */ + setIdBaseUrl(url) { + this.opts.idBaseUrl = url; + } + idServerRequest(method, path, params, prefix, accessToken) { + if (!this.opts.idBaseUrl) { + throw new Error("No identity server base URL set"); + } + let queryParams = undefined; + let body = undefined; + if (method === _method.Method.Get) { + queryParams = params; + } else { + body = params; + } + const fullUri = this.getUrl(path, queryParams, prefix, this.opts.idBaseUrl); + const opts = { + json: true, + headers: {} + }; + if (accessToken) { + opts.headers.Authorization = `Bearer ${accessToken}`; + } + return this.requestOtherUrl(method, fullUri, body, opts); + } + + /** + * Perform an authorised request to the homeserver. + * @param method - The HTTP method e.g. "GET". + * @param path - The HTTP path after the supplied prefix e.g. + * "/createRoom". + * + * @param queryParams - A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param body - The HTTP JSON body. + * + * @param opts - additional options. If a number is specified, + * this is treated as `opts.localTimeoutMs`. + * + * @returns Promise which resolves to + * ``` + * { + * data: {Object}, + * headers: {Object}, + * code: {Number}, + * } + * ``` + * If `onlyData` is set, this will resolve to the `data` object only. + * @returns Rejects with an error if a problem occurred. + * This includes network problems and Matrix-specific error JSON. + */ + authedRequest(method, path, queryParams, body, opts = {}) { + if (!queryParams) queryParams = {}; + if (this.opts.accessToken) { + if (this.opts.useAuthorizationHeader) { + if (!opts.headers) { + opts.headers = {}; + } + if (!opts.headers.Authorization) { + opts.headers.Authorization = "Bearer " + this.opts.accessToken; + } + if (queryParams.access_token) { + delete queryParams.access_token; + } + } else if (!queryParams.access_token) { + queryParams.access_token = this.opts.accessToken; + } + } + const requestPromise = this.request(method, path, queryParams, body, opts); + requestPromise.catch(err => { + if (err.errcode == "M_UNKNOWN_TOKEN" && !opts?.inhibitLogoutEmit) { + this.eventEmitter.emit(_interface.HttpApiEvent.SessionLoggedOut, err); + } else if (err.errcode == "M_CONSENT_NOT_GIVEN") { + this.eventEmitter.emit(_interface.HttpApiEvent.NoConsent, err.message, err.data.consent_uri); + } + }); + + // return the original promise, otherwise tests break due to it having to + // go around the event loop one more time to process the result of the request + return requestPromise; + } + + /** + * Perform a request to the homeserver without any credentials. + * @param method - The HTTP method e.g. "GET". + * @param path - The HTTP path after the supplied prefix e.g. + * "/createRoom". + * + * @param queryParams - A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * + * @param body - The HTTP JSON body. + * + * @param opts - additional options + * + * @returns Promise which resolves to + * ``` + * { + * data: {Object}, + * headers: {Object}, + * code: {Number}, + * } + * ``` + * If `onlyData is set, this will resolve to the data` + * object only. + * @returns Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + request(method, path, queryParams, body, opts) { + const fullUri = this.getUrl(path, queryParams, opts?.prefix, opts?.baseUrl); + return this.requestOtherUrl(method, fullUri, body, opts); + } + + /** + * Perform a request to an arbitrary URL. + * @param method - The HTTP method e.g. "GET". + * @param url - The HTTP URL object. + * + * @param body - The HTTP JSON body. + * + * @param opts - additional options + * + * @returns Promise which resolves to data unless `onlyData` is specified as false, + * where the resolved value will be a fetch Response object. + * @returns Rejects with an error if a problem + * occurred. This includes network problems and Matrix-specific error JSON. + */ + async requestOtherUrl(method, url, body, opts = {}) { + const headers = Object.assign({}, opts.headers || {}); + const json = opts.json ?? true; + // We can't use getPrototypeOf here as objects made in other contexts e.g. over postMessage won't have same ref + const jsonBody = json && body?.constructor?.name === Object.name; + if (json) { + if (jsonBody && !headers["Content-Type"]) { + headers["Content-Type"] = "application/json"; + } + if (!headers["Accept"]) { + headers["Accept"] = "application/json"; + } + } + const timeout = opts.localTimeoutMs ?? this.opts.localTimeoutMs; + const keepAlive = opts.keepAlive ?? false; + const signals = [this.abortController.signal]; + if (timeout !== undefined) { + signals.push((0, _utils2.timeoutSignal)(timeout)); + } + if (opts.abortSignal) { + signals.push(opts.abortSignal); + } + let data; + if (jsonBody) { + data = JSON.stringify(body); + } else { + data = body; + } + const { + signal, + cleanup + } = (0, _utils2.anySignal)(signals); + let res; + try { + res = await this.fetch(url, { + signal, + method, + body: data, + headers, + mode: "cors", + redirect: "follow", + referrer: "", + referrerPolicy: "no-referrer", + cache: "no-cache", + credentials: "omit", + // we send credentials via headers + keepalive: keepAlive + }); + } catch (e) { + if (e.name === "AbortError") { + throw e; + } + throw new _errors.ConnectionError("fetch failed", e); + } finally { + cleanup(); + } + if (!res.ok) { + throw (0, _utils2.parseErrorResponse)(res, await res.text()); + } + if (this.opts.onlyData) { + return json ? res.json() : res.text(); + } + return res; + } + + /** + * Form and return a homeserver request URL based on the given path params and prefix. + * @param path - The HTTP path after the supplied prefix e.g. "/createRoom". + * @param queryParams - A dict of query params (these will NOT be urlencoded). + * @param prefix - The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix. + * @param baseUrl - The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl. + * @returns URL + */ + getUrl(path, queryParams, prefix, baseUrl) { + const url = new URL((baseUrl ?? this.opts.baseUrl) + (prefix ?? this.opts.prefix) + path); + if (queryParams) { + utils.encodeParams(queryParams, url.searchParams); + } + return url; + } +} +exports.FetchHttpApi = FetchHttpApi; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/index.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,226 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _exportNames = { + MatrixHttpApi: true +}; +exports.MatrixHttpApi = void 0; +var _fetch = require("./fetch"); +var _prefix = require("./prefix"); +Object.keys(_prefix).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _prefix[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _prefix[key]; + } + }); +}); +var utils = _interopRequireWildcard(require("../utils")); +var callbacks = _interopRequireWildcard(require("../realtime-callbacks")); +var _method = require("./method"); +Object.keys(_method).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _method[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _method[key]; + } + }); +}); +var _errors = require("./errors"); +Object.keys(_errors).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _errors[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _errors[key]; + } + }); +}); +var _utils2 = require("./utils"); +Object.keys(_utils2).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _utils2[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _utils2[key]; + } + }); +}); +var _interface = require("./interface"); +Object.keys(_interface).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _interface[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _interface[key]; + } + }); +}); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +class MatrixHttpApi extends _fetch.FetchHttpApi { + constructor(...args) { + super(...args); + _defineProperty(this, "uploads", []); + } + /** + * Upload content to the homeserver + * + * @param file - The object to upload. On a browser, something that + * can be sent to XMLHttpRequest.send (typically a File). Under node.js, + * a Buffer, String or ReadStream. + * + * @param opts - options object + * + * @returns Promise which resolves to response object, as + * determined by this.opts.onlyData, opts.rawResponse, and + * opts.onlyContentUri. Rejects with an error (usually a MatrixError). + */ + uploadContent(file, opts = {}) { + const includeFilename = opts.includeFilename ?? true; + const abortController = opts.abortController ?? new AbortController(); + + // If the file doesn't have a mime type, use a default since the HS errors if we don't supply one. + const contentType = opts.type ?? file.type ?? "application/octet-stream"; + const fileName = opts.name ?? file.name; + const upload = { + loaded: 0, + total: 0, + abortController + }; + const defer = utils.defer(); + if (global.XMLHttpRequest) { + const xhr = new global.XMLHttpRequest(); + const timeoutFn = function () { + xhr.abort(); + defer.reject(new Error("Timeout")); + }; + + // set an initial timeout of 30s; we'll advance it each time we get a progress notification + let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); + xhr.onreadystatechange = function () { + switch (xhr.readyState) { + case global.XMLHttpRequest.DONE: + callbacks.clearTimeout(timeoutTimer); + try { + if (xhr.status === 0) { + throw new DOMException(xhr.statusText, "AbortError"); // mimic fetch API + } + + if (!xhr.responseText) { + throw new Error("No response body."); + } + if (xhr.status >= 400) { + defer.reject((0, _utils2.parseErrorResponse)(xhr, xhr.responseText)); + } else { + defer.resolve(JSON.parse(xhr.responseText)); + } + } catch (err) { + if (err.name === "AbortError") { + defer.reject(err); + return; + } + defer.reject(new _errors.ConnectionError("request failed", err)); + } + break; + } + }; + xhr.upload.onprogress = ev => { + callbacks.clearTimeout(timeoutTimer); + upload.loaded = ev.loaded; + upload.total = ev.total; + timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); + opts.progressHandler?.({ + loaded: ev.loaded, + total: ev.total + }); + }; + const url = this.getUrl("/upload", undefined, _prefix.MediaPrefix.R0); + if (includeFilename && fileName) { + url.searchParams.set("filename", encodeURIComponent(fileName)); + } + if (!this.opts.useAuthorizationHeader && this.opts.accessToken) { + url.searchParams.set("access_token", encodeURIComponent(this.opts.accessToken)); + } + xhr.open(_method.Method.Post, url.href); + if (this.opts.useAuthorizationHeader && this.opts.accessToken) { + xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken); + } + xhr.setRequestHeader("Content-Type", contentType); + xhr.send(file); + abortController.signal.addEventListener("abort", () => { + xhr.abort(); + }); + } else { + const queryParams = {}; + if (includeFilename && fileName) { + queryParams.filename = fileName; + } + const headers = { + "Content-Type": contentType + }; + this.authedRequest(_method.Method.Post, "/upload", queryParams, file, { + prefix: _prefix.MediaPrefix.R0, + headers, + abortSignal: abortController.signal + }).then(response => { + return this.opts.onlyData ? response : response.json(); + }).then(defer.resolve, defer.reject); + } + + // remove the upload from the list on completion + upload.promise = defer.promise.finally(() => { + utils.removeElement(this.uploads, elem => elem === upload); + }); + abortController.signal.addEventListener("abort", () => { + utils.removeElement(this.uploads, elem => elem === upload); + defer.reject(new DOMException("Aborted", "AbortError")); + }); + this.uploads.push(upload); + return upload.promise; + } + cancelUpload(promise) { + const upload = this.uploads.find(u => u.promise === promise); + if (upload) { + upload.abortController.abort(); + return true; + } + return false; + } + getCurrentUploads() { + return this.uploads; + } + + /** + * Get the content repository url with query parameters. + * @returns An object with a 'base', 'path' and 'params' for base URL, + * path and query parameters respectively. + */ + getContentUri() { + return { + base: this.opts.baseUrl, + path: _prefix.MediaPrefix.R0 + "/upload", + params: { + access_token: this.opts.accessToken + } + }; + } +} +exports.MatrixHttpApi = MatrixHttpApi; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/interface.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/interface.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/interface.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/interface.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,27 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.HttpApiEvent = void 0; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +let HttpApiEvent; +exports.HttpApiEvent = HttpApiEvent; +(function (HttpApiEvent) { + HttpApiEvent["SessionLoggedOut"] = "Session.logged_out"; + HttpApiEvent["NoConsent"] = "no_consent"; +})(HttpApiEvent || (exports.HttpApiEvent = HttpApiEvent = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/method.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/method.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/method.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/method.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,29 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Method = void 0; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +let Method; +exports.Method = Method; +(function (Method) { + Method["Get"] = "GET"; + Method["Put"] = "PUT"; + Method["Post"] = "POST"; + Method["Delete"] = "DELETE"; +})(Method || (exports.Method = Method = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/prefix.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/prefix.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/prefix.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/prefix.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,39 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MediaPrefix = exports.IdentityPrefix = exports.ClientPrefix = void 0; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +let ClientPrefix; +exports.ClientPrefix = ClientPrefix; +(function (ClientPrefix) { + ClientPrefix["R0"] = "/_matrix/client/r0"; + ClientPrefix["V1"] = "/_matrix/client/v1"; + ClientPrefix["V3"] = "/_matrix/client/v3"; + ClientPrefix["Unstable"] = "/_matrix/client/unstable"; +})(ClientPrefix || (exports.ClientPrefix = ClientPrefix = {})); +let IdentityPrefix; +exports.IdentityPrefix = IdentityPrefix; +(function (IdentityPrefix) { + IdentityPrefix["V2"] = "/_matrix/identity/v2"; +})(IdentityPrefix || (exports.IdentityPrefix = IdentityPrefix = {})); +let MediaPrefix; +exports.MediaPrefix = MediaPrefix; +(function (MediaPrefix) { + MediaPrefix["R0"] = "/_matrix/media/r0"; +})(MediaPrefix || (exports.MediaPrefix = MediaPrefix = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/utils.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/utils.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/utils.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api/utils.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,143 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.anySignal = anySignal; +exports.parseErrorResponse = parseErrorResponse; +exports.retryNetworkOperation = retryNetworkOperation; +exports.timeoutSignal = timeoutSignal; +var _contentType = require("content-type"); +var _logger = require("../logger"); +var _utils = require("../utils"); +var _errors = require("./errors"); +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout +function timeoutSignal(ms) { + const controller = new AbortController(); + setTimeout(() => { + controller.abort(); + }, ms); + return controller.signal; +} +function anySignal(signals) { + const controller = new AbortController(); + function cleanup() { + for (const signal of signals) { + signal.removeEventListener("abort", onAbort); + } + } + function onAbort() { + controller.abort(); + cleanup(); + } + for (const signal of signals) { + if (signal.aborted) { + onAbort(); + break; + } + signal.addEventListener("abort", onAbort); + } + return { + signal: controller.signal, + cleanup + }; +} + +/** + * Attempt to turn an HTTP error response into a Javascript Error. + * + * If it is a JSON response, we will parse it into a MatrixError. Otherwise + * we return a generic Error. + * + * @param response - response object + * @param body - raw body of the response + * @returns + */ +function parseErrorResponse(response, body) { + let contentType; + try { + contentType = getResponseContentType(response); + } catch (e) { + return e; + } + if (contentType?.type === "application/json" && body) { + return new _errors.MatrixError(JSON.parse(body), response.status, isXhr(response) ? response.responseURL : response.url); + } + if (contentType?.type === "text/plain") { + return new _errors.HTTPError(`Server returned ${response.status} error: ${body}`, response.status); + } + return new _errors.HTTPError(`Server returned ${response.status} error`, response.status); +} +function isXhr(response) { + return "getResponseHeader" in response; +} + +/** + * extract the Content-Type header from the response object, and + * parse it to a `{type, parameters}` object. + * + * returns null if no content-type header could be found. + * + * @param response - response object + * @returns parsed content-type header, or null if not found + */ +function getResponseContentType(response) { + let contentType; + if (isXhr(response)) { + contentType = response.getResponseHeader("Content-Type"); + } else { + contentType = response.headers.get("Content-Type"); + } + if (!contentType) return null; + try { + return (0, _contentType.parse)(contentType); + } catch (e) { + throw new Error(`Error parsing Content-Type '${contentType}': ${e}`); + } +} + +/** + * Retries a network operation run in a callback. + * @param maxAttempts - maximum attempts to try + * @param callback - callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again. + * @returns the result of the network operation + * @throws {@link ConnectionError} If after maxAttempts the callback still throws ConnectionError + */ +async function retryNetworkOperation(maxAttempts, callback) { + let attempts = 0; + let lastConnectionError = null; + while (attempts < maxAttempts) { + try { + if (attempts > 0) { + const timeout = 1000 * Math.pow(2, attempts); + _logger.logger.log(`network operation failed ${attempts} times, retrying in ${timeout}ms...`); + await (0, _utils.sleep)(timeout); + } + return await callback(); + } catch (err) { + if (err instanceof _errors.ConnectionError) { + attempts += 1; + lastConnectionError = err; + } else { + throw err; + } + } + } + throw lastConnectionError; +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/http-api.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/http-api.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,1027 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.PREFIX_V3 = exports.PREFIX_V1 = exports.PREFIX_UNSTABLE = exports.PREFIX_R0 = exports.PREFIX_MEDIA_R0 = exports.PREFIX_IDENTITY_V2 = exports.PREFIX_IDENTITY_V1 = exports.Method = exports.MatrixHttpApi = exports.MatrixError = exports.HttpApiEvent = exports.ConnectionError = exports.AbortError = void 0; -exports.retryNetworkOperation = retryNetworkOperation; - -var _contentType = require("content-type"); - -var callbacks = _interopRequireWildcard(require("./realtime-callbacks")); - -var utils = _interopRequireWildcard(require("./utils")); - -var _logger = require("./logger"); - -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - -function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -/* -TODO: -- CS: complete register function (doing stages) -- Identity server: linkEmail, authEmail, bindEmail, lookup3pid -*/ - -/** - * A constant representing the URI path for release 0 of the Client-Server HTTP API. - */ -const PREFIX_R0 = "/_matrix/client/r0"; -/** - * A constant representing the URI path for the legacy release v1 of the Client-Server HTTP API. - */ - -exports.PREFIX_R0 = PREFIX_R0; -const PREFIX_V1 = "/_matrix/client/v1"; -/** - * A constant representing the URI path for Client-Server API endpoints versioned at v3. - */ - -exports.PREFIX_V1 = PREFIX_V1; -const PREFIX_V3 = "/_matrix/client/v3"; -/** - * A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs. - */ - -exports.PREFIX_V3 = PREFIX_V3; -const PREFIX_UNSTABLE = "/_matrix/client/unstable"; -/** - * URI path for v1 of the the identity API - * @deprecated Use v2. - */ - -exports.PREFIX_UNSTABLE = PREFIX_UNSTABLE; -const PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1"; -/** - * URI path for the v2 identity API - */ - -exports.PREFIX_IDENTITY_V1 = PREFIX_IDENTITY_V1; -const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2"; -/** - * URI path for the media repo API - */ - -exports.PREFIX_IDENTITY_V2 = PREFIX_IDENTITY_V2; -const PREFIX_MEDIA_R0 = "/_matrix/media/r0"; -exports.PREFIX_MEDIA_R0 = PREFIX_MEDIA_R0; -let Method; -exports.Method = Method; - -(function (Method) { - Method["Get"] = "GET"; - Method["Put"] = "PUT"; - Method["Post"] = "POST"; - Method["Delete"] = "DELETE"; -})(Method || (exports.Method = Method = {})); - -let HttpApiEvent; -exports.HttpApiEvent = HttpApiEvent; - -(function (HttpApiEvent) { - HttpApiEvent["SessionLoggedOut"] = "Session.logged_out"; - HttpApiEvent["NoConsent"] = "no_consent"; -})(HttpApiEvent || (exports.HttpApiEvent = HttpApiEvent = {})); - -/** - * Construct a MatrixHttpApi. - * @constructor - * @param {EventEmitter} eventEmitter The event emitter to use for emitting events - * @param {Object} opts The options to use for this HTTP API. - * @param {string} opts.baseUrl Required. The base client-server URL e.g. - * 'http://localhost:8008'. - * @param {Function} opts.request Required. The function to call for HTTP - * requests. This function must look like function(opts, callback){ ... }. - * @param {string} opts.prefix Required. The matrix client prefix to use, e.g. - * '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants. - * - * @param {boolean} opts.onlyData True to return only the 'data' component of the - * response (e.g. the parsed HTTP body). If false, requests will return an - * object with the properties code, headers and data. - * - * @param {string=} opts.accessToken The access_token to send with requests. Can be - * null to not send an access token. - * @param {Object=} opts.extraParams Optional. Extra query parameters to send on - * requests. - * @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait - * before timing out the request. If not specified, there is no timeout. - * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use - * Authorization header instead of query param to send the access token to the server. - */ -class MatrixHttpApi { - constructor(eventEmitter, opts) { - this.eventEmitter = eventEmitter; - this.opts = opts; - - _defineProperty(this, "uploads", []); - - utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]); - opts.onlyData = !!opts.onlyData; - opts.useAuthorizationHeader = !!opts.useAuthorizationHeader; - } - /** - * Sets the base URL for the identity server - * @param {string} url The new base url - */ - - - setIdBaseUrl(url) { - this.opts.idBaseUrl = url; - } - /** - * Get the content repository url with query parameters. - * @return {Object} An object with a 'base', 'path' and 'params' for base URL, - * path and query parameters respectively. - */ - - - getContentUri() { - return { - base: this.opts.baseUrl, - path: "/_matrix/media/r0/upload", - params: { - access_token: this.opts.accessToken - } - }; - } - /** - * Upload content to the homeserver - * - * @param {object} file The object to upload. On a browser, something that - * can be sent to XMLHttpRequest.send (typically a File). Under node.js, - * a Buffer, String or ReadStream. - * - * @param {object} opts options object - * - * @param {string=} opts.name Name to give the file on the server. Defaults - * to file.name. - * - * @param {boolean=} opts.includeFilename if false will not send the filename, - * e.g for encrypted file uploads where filename leaks are undesirable. - * Defaults to true. - * - * @param {string=} opts.type Content-type for the upload. Defaults to - * file.type, or applicaton/octet-stream. - * - * @param {boolean=} opts.rawResponse Return the raw body, rather than - * parsing the JSON. Defaults to false (except on node.js, where it - * defaults to true for backwards compatibility). - * - * @param {boolean=} opts.onlyContentUri Just return the content URI, - * rather than the whole body. Defaults to false (except on browsers, - * where it defaults to true for backwards compatibility). Ignored if - * opts.rawResponse is true. - * - * @param {Function=} opts.callback Deprecated. Optional. The callback to - * invoke on success/failure. See the promise return values for more - * information. - * - * @param {Function=} opts.progressHandler Optional. Called when a chunk of - * data has been uploaded, with an object containing the fields `loaded` - * (number of bytes transferred) and `total` (total size, if known). - * - * @return {Promise} Resolves to response object, as - * determined by this.opts.onlyData, opts.rawResponse, and - * opts.onlyContentUri. Rejects with an error (usually a MatrixError). - */ - - - uploadContent(file, opts) { - if (utils.isFunction(opts)) { - // opts used to be callback, backwards compatibility - opts = { - callback: opts - }; - } else if (!opts) { - opts = {}; - } // default opts.includeFilename to true (ignoring falsey values) - - - const includeFilename = opts.includeFilename !== false; // if the file doesn't have a mime type, use a default since - // the HS errors if we don't supply one. - - const contentType = opts.type || file.type || 'application/octet-stream'; - const fileName = opts.name || file.name; // We used to recommend setting file.stream to the thing to upload on - // Node.js. As of 2019-06-11, this is still in widespread use in various - // clients, so we should preserve this for simple objects used in - // Node.js. File API objects (via either the File or Blob interfaces) in - // the browser now define a `stream` method, which leads to trouble - // here, so we also check the type of `stream`. - - let body = file; - const bodyStream = body.stream; // this type is wrong but for legacy reasons is good enough - - if (bodyStream && typeof bodyStream !== "function") { - _logger.logger.warn("Using `file.stream` as the content to upload. Future " + "versions of the js-sdk will change this to expect `file` to " + "be the content directly."); - - body = bodyStream; - } // backwards-compatibility hacks where we used to do different things - // between browser and node. - - - let rawResponse = opts.rawResponse; - - if (rawResponse === undefined) { - if (global.XMLHttpRequest) { - rawResponse = false; - } else { - _logger.logger.warn("Returning the raw JSON from uploadContent(). Future " + "versions of the js-sdk will change this default, to " + "return the parsed object. Set opts.rawResponse=false " + "to change this behaviour now."); - - rawResponse = true; - } - } - - let onlyContentUri = opts.onlyContentUri; - - if (!rawResponse && onlyContentUri === undefined) { - if (global.XMLHttpRequest) { - _logger.logger.warn("Returning only the content-uri from uploadContent(). " + "Future versions of the js-sdk will change this " + "default, to return the whole response object. Set " + "opts.onlyContentUri=false to change this behaviour now."); - - onlyContentUri = true; - } else { - onlyContentUri = false; - } - } // browser-request doesn't support File objects because it deep-copies - // the options using JSON.parse(JSON.stringify(options)). Instead of - // loading the whole file into memory as a string and letting - // browser-request base64 encode and then decode it again, we just - // use XMLHttpRequest directly. - // (browser-request doesn't support progress either, which is also kind - // of important here) - - - const upload = { - loaded: 0, - total: 0 - }; - let promise; // XMLHttpRequest doesn't parse JSON for us. request normally does, but - // we're setting opts.json=false so that it doesn't JSON-encode the - // request, which also means it doesn't JSON-decode the response. Either - // way, we have to JSON-parse the response ourselves. - - let bodyParser = null; - - if (!rawResponse) { - bodyParser = function (rawBody) { - let body = JSON.parse(rawBody); - - if (onlyContentUri) { - body = body.content_uri; - - if (body === undefined) { - throw Error('Bad response'); - } - } - - return body; - }; - } - - if (global.XMLHttpRequest) { - const defer = utils.defer(); - const xhr = new global.XMLHttpRequest(); - const cb = requestCallback(defer, opts.callback, this.opts.onlyData); - - const timeoutFn = function () { - xhr.abort(); - cb(new Error('Timeout')); - }; // set an initial timeout of 30s; we'll advance it each time we get a progress notification - - - let timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); - - xhr.onreadystatechange = function () { - let resp; - - switch (xhr.readyState) { - case global.XMLHttpRequest.DONE: - callbacks.clearTimeout(timeoutTimer); - - try { - if (xhr.status === 0) { - throw new AbortError(); - } - - if (!xhr.responseText) { - throw new Error('No response body.'); - } - - resp = xhr.responseText; - - if (bodyParser) { - resp = bodyParser(resp); - } - } catch (err) { - err.httpStatus = xhr.status; - cb(err); - return; - } - - cb(undefined, xhr, resp); - break; - } - }; - - xhr.upload.addEventListener("progress", function (ev) { - callbacks.clearTimeout(timeoutTimer); - upload.loaded = ev.loaded; - upload.total = ev.total; - timeoutTimer = callbacks.setTimeout(timeoutFn, 30000); - - if (opts.progressHandler) { - opts.progressHandler({ - loaded: ev.loaded, - total: ev.total - }); - } - }); - let url = this.opts.baseUrl + "/_matrix/media/r0/upload"; - const queryArgs = []; - - if (includeFilename && fileName) { - queryArgs.push("filename=" + encodeURIComponent(fileName)); - } - - if (!this.opts.useAuthorizationHeader) { - queryArgs.push("access_token=" + encodeURIComponent(this.opts.accessToken)); - } - - if (queryArgs.length > 0) { - url += "?" + queryArgs.join("&"); - } - - xhr.open("POST", url); - - if (this.opts.useAuthorizationHeader) { - xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken); - } - - xhr.setRequestHeader("Content-Type", contentType); - xhr.send(body); - promise = defer.promise; // dirty hack (as per doRequest) to allow the upload to be cancelled. - - promise.abort = xhr.abort.bind(xhr); - } else { - const queryParams = {}; - - if (includeFilename && fileName) { - queryParams.filename = fileName; - } - - const headers = { - "Content-Type": contentType - }; // authedRequest uses `request` which is no longer maintained. - // `request` has a bug where if the body is zero bytes then you get an error: `Argument error, options.body`. - // See https://github.com/request/request/issues/920 - // if body looks like a byte array and empty then set the Content-Length explicitly as a workaround: - - if (body.length === 0) { - headers["Content-Length"] = "0"; - } - - promise = this.authedRequest(opts.callback, Method.Post, "/upload", queryParams, body, { - prefix: "/_matrix/media/r0", - headers, - json: false, - bodyParser - }); - } // remove the upload from the list on completion - - - upload.promise = promise.finally(() => { - for (let i = 0; i < this.uploads.length; ++i) { - if (this.uploads[i] === upload) { - this.uploads.splice(i, 1); - return; - } - } - }); // copy our dirty abort() method to the new promise - - upload.promise.abort = promise.abort; - this.uploads.push(upload); - return upload.promise; - } - - cancelUpload(promise) { - if (promise.abort) { - promise.abort(); - return true; - } - - return false; - } - - getCurrentUploads() { - return this.uploads; - } - - idServerRequest(callback, method, path, params, prefix, accessToken) { - if (!this.opts.idBaseUrl) { - throw new Error("No identity server base URL set"); - } - - const fullUri = this.opts.idBaseUrl + prefix + path; - - if (callback !== undefined && !utils.isFunction(callback)) { - throw Error("Expected callback to be a function but got " + typeof callback); - } - - const opts = { - uri: fullUri, - method, - withCredentials: false, - json: true, - // we want a JSON response if we can - _matrix_opts: this.opts, - headers: {} - }; - - if (method === Method.Get) { - opts.qs = params; - } else if (typeof params === "object") { - opts.json = params; - } - - if (accessToken) { - opts.headers['Authorization'] = `Bearer ${accessToken}`; - } - - const defer = utils.defer(); - this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData)); - return defer.promise; - } - /** - * Perform an authorised request to the homeserver. - * @param {Function} callback Optional. The callback to invoke on - * success/failure. See the promise return values for more information. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} path The HTTP path after the supplied prefix e.g. - * "/createRoom". - * - * @param {Object=} queryParams A dict of query params (these will NOT be - * urlencoded). If unspecified, there will be no query params. - * - * @param {Object} [data] The HTTP JSON body. - * - * @param {Object|Number=} opts additional options. If a number is specified, - * this is treated as `opts.localTimeoutMs`. - * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {string=} opts.baseUrl The alternative base url to use. - * If not specified, uses this.opts.baseUrl - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data - * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem - * occurred. This includes network problems and Matrix-specific error JSON. - */ - - - authedRequest(callback, method, path, queryParams, data, opts // number is legacy - ) { - if (!queryParams) queryParams = {}; - let requestOpts = opts || {}; - - if (this.opts.useAuthorizationHeader) { - if (isFinite(opts)) { - // opts used to be localTimeoutMs - requestOpts = { - localTimeoutMs: opts - }; - } - - if (!requestOpts.headers) { - requestOpts.headers = {}; - } - - if (!requestOpts.headers.Authorization) { - requestOpts.headers.Authorization = "Bearer " + this.opts.accessToken; - } - - if (queryParams.access_token) { - delete queryParams.access_token; - } - } else if (!queryParams.access_token) { - queryParams.access_token = this.opts.accessToken; - } - - const requestPromise = this.request(callback, method, path, queryParams, data, requestOpts); - requestPromise.catch(err => { - if (err.errcode == 'M_UNKNOWN_TOKEN' && !requestOpts?.inhibitLogoutEmit) { - this.eventEmitter.emit(HttpApiEvent.SessionLoggedOut, err); - } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') { - this.eventEmitter.emit(HttpApiEvent.NoConsent, err.message, err.data.consent_uri); - } - }); // return the original promise, otherwise tests break due to it having to - // go around the event loop one more time to process the result of the request - - return requestPromise; - } - /** - * Perform a request to the homeserver without any credentials. - * @param {Function} callback Optional. The callback to invoke on - * success/failure. See the promise return values for more information. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} path The HTTP path after the supplied prefix e.g. - * "/createRoom". - * - * @param {Object=} queryParams A dict of query params (these will NOT be - * urlencoded). If unspecified, there will be no query params. - * - * @param {Object} [data] The HTTP JSON body. - * - * @param {Object=} opts additional options - * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data - * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem - * occurred. This includes network problems and Matrix-specific error JSON. - */ - - - request(callback, method, path, queryParams, data, opts) { - const prefix = opts?.prefix ?? this.opts.prefix; - const baseUrl = opts?.baseUrl ?? this.opts.baseUrl; - const fullUri = baseUrl + prefix + path; - return this.requestOtherUrl(callback, method, fullUri, queryParams, data, opts); - } - /** - * Perform a request to an arbitrary URL. - * @param {Function} callback Optional. The callback to invoke on - * success/failure. See the promise return values for more information. - * @param {string} method The HTTP method e.g. "GET". - * @param {string} uri The HTTP URI - * - * @param {Object=} queryParams A dict of query params (these will NOT be - * urlencoded). If unspecified, there will be no query params. - * - * @param {Object} [data] The HTTP JSON body. - * - * @param {Object=} opts additional options - * - * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before - * timing out the request. If not specified, there is no timeout. - * - * @param {string=} opts.prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. - * - * @param {Object=} opts.headers map of additional request headers - * - * @return {Promise} Resolves to {data: {Object}, - * headers: {Object}, code: {Number}}. - * If onlyData is set, this will resolve to the data - * object only. - * @return {module:http-api.MatrixError} Rejects with an error if a problem - * occurred. This includes network problems and Matrix-specific error JSON. - */ - - - requestOtherUrl(callback, method, uri, queryParams, data, opts // number is legacy - ) { - let requestOpts = opts || {}; - - if (isFinite(opts)) { - // opts used to be localTimeoutMs - requestOpts = { - localTimeoutMs: opts - }; - } - - return this.doRequest(callback, method, uri, queryParams, data, requestOpts); - } - /** - * Form and return a homeserver request URL based on the given path - * params and prefix. - * @param {string} path The HTTP path after the supplied prefix e.g. - * "/createRoom". - * @param {Object} queryParams A dict of query params (these will NOT be - * urlencoded). - * @param {string} prefix The full prefix to use e.g. - * "/_matrix/client/v2_alpha". - * @return {string} URL - */ - - - getUrl(path, queryParams, prefix) { - let queryString = ""; - - if (queryParams) { - queryString = "?" + utils.encodeParams(queryParams); - } - - return this.opts.baseUrl + prefix + path + queryString; - } - /** - * @private - * - * @param {function} callback - * @param {string} method - * @param {string} uri - * @param {object} queryParams - * @param {object|string} data - * @param {object=} opts - * - * @param {boolean} [opts.json =true] Json-encode data before sending, and - * decode response on receipt. (We will still json-decode error - * responses, even if this is false.) - * - * @param {object=} opts.headers extra request headers - * - * @param {number=} opts.localTimeoutMs client-side timeout for the - * request. Default timeout if falsy. - * - * @param {function=} opts.bodyParser function to parse the body of the - * response before passing it to the promise and callback. - * - * @return {Promise} a promise which resolves to either the - * response object (if this.opts.onlyData is truthy), or the parsed - * body. Rejects - * - * Generic T is the callback/promise resolve type - * Generic O should be inferred - */ - - - doRequest(callback, method, uri, queryParams, data, opts) { - if (callback !== undefined && !utils.isFunction(callback)) { - throw Error("Expected callback to be a function but got " + typeof callback); - } - - if (this.opts.extraParams) { - queryParams = _objectSpread(_objectSpread({}, queryParams || {}), this.opts.extraParams); - } - - const headers = Object.assign({}, opts.headers || {}); - if (!opts) opts = {}; - const json = opts.json ?? true; - let bodyParser = opts.bodyParser; // we handle the json encoding/decoding here, because request and - // browser-request make a mess of it. Specifically, they attempt to - // json-decode plain-text error responses, which in turn means that the - // actual error gets swallowed by a SyntaxError. - - if (json) { - if (data) { - data = JSON.stringify(data); - headers['content-type'] = 'application/json'; - } - - if (!headers['accept']) { - headers['accept'] = 'application/json'; - } - - if (bodyParser === undefined) { - bodyParser = function (rawBody) { - return JSON.parse(rawBody); - }; - } - } - - const defer = utils.defer(); - let timeoutId; - let timedOut = false; - let req; - const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs; - - const resetTimeout = () => { - if (localTimeoutMs) { - if (timeoutId) { - callbacks.clearTimeout(timeoutId); - } - - timeoutId = callbacks.setTimeout(function () { - timedOut = true; - req?.abort?.(); - defer.reject(new MatrixError({ - error: "Locally timed out waiting for a response", - errcode: "ORG.MATRIX.JSSDK_TIMEOUT", - timeout: localTimeoutMs - })); - }, localTimeoutMs); - } - }; - - resetTimeout(); - const reqPromise = defer.promise; - - try { - req = this.opts.request({ - uri: uri, - method: method, - withCredentials: false, - qs: queryParams, - qsStringifyOptions: opts.qsStringifyOptions, - useQuerystring: true, - body: data, - json: false, - timeout: localTimeoutMs, - headers: headers || {}, - _matrix_opts: this.opts - }, (err, response, body) => { - if (localTimeoutMs) { - callbacks.clearTimeout(timeoutId); - - if (timedOut) { - return; // already rejected promise - } - } - - const handlerFn = requestCallback(defer, callback, this.opts.onlyData, bodyParser); - handlerFn(err, response, body); - }); - - if (req) { - // This will only work in a browser, where opts.request is the - // `browser-request` import. Currently, `request` does not support progress - // updates - see https://github.com/request/request/pull/2346. - // `browser-request` returns an XHRHttpRequest which exposes `onprogress` - if ('onprogress' in req) { - req.onprogress = e => { - // Prevent the timeout from rejecting the deferred promise if progress is - // seen with the request - resetTimeout(); - }; - } // FIXME: This is EVIL, but I can't think of a better way to expose - // abort() operations on underlying HTTP requests :( - - - if (req.abort) { - reqPromise.abort = req.abort.bind(req); - } - } - } catch (ex) { - defer.reject(ex); - - if (callback) { - callback(ex); - } - } - - return reqPromise; - } - -} - -exports.MatrixHttpApi = MatrixHttpApi; - -function getStatusCode(response) { - return response.status || response.statusCode; -} -/* - * Returns a callback that can be invoked by an HTTP request on completion, - * that will either resolve or reject the given defer as well as invoke the - * given userDefinedCallback (if any). - * - * HTTP errors are transformed into javascript errors and the deferred is rejected. - * - * If bodyParser is given, it is used to transform the body of the successful - * responses before passing to the defer/callback. - * - * If onlyData is true, the defer/callback is invoked with the body of the - * response, otherwise the result object (with `code` and `data` fields) - * - */ - - -function requestCallback(defer, userDefinedCallback, onlyData = false, bodyParser) { - return function (err, response, body) { - if (err) { - // the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request. - // See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48 - const aborted = err.name === "AbortError" || err === "aborted"; - - if (!aborted && !(err instanceof MatrixError)) { - // browser-request just throws normal Error objects, - // not `TypeError`s like fetch does. So just assume any - // error is due to the connection. - err = new ConnectionError("request failed", err); - } - } - - let data = body; - - if (!err) { - try { - if (getStatusCode(response) >= 400) { - err = parseErrorResponse(response, body); - } else if (bodyParser) { - data = bodyParser(body); - } - } catch (e) { - err = new Error(`Error parsing server response: ${e}`); - } - } - - if (err) { - defer.reject(err); - userDefinedCallback?.(err); - } else if (onlyData) { - defer.resolve(data); - userDefinedCallback?.(null, data); - } else { - const res = { - code: getStatusCode(response), - // XXX: why do we bother with this? it doesn't work for - // XMLHttpRequest, so clearly we don't use it. - headers: response.headers, - data: data - }; // XXX: the variations in caller-expected types here are horrible, - // typescript doesn't do conditional types based on runtime values - - defer.resolve(res); - userDefinedCallback?.(null, res); - } - }; -} -/** - * Attempt to turn an HTTP error response into a Javascript Error. - * - * If it is a JSON response, we will parse it into a MatrixError. Otherwise - * we return a generic Error. - * - * @param {XMLHttpRequest|http.IncomingMessage} response response object - * @param {String} body raw body of the response - * @returns {Error} - */ - - -function parseErrorResponse(response, body) { - const httpStatus = getStatusCode(response); - const contentType = getResponseContentType(response); - let err; - - if (contentType) { - if (contentType.type === 'application/json') { - const jsonBody = typeof body === 'object' ? body : JSON.parse(body); - err = new MatrixError(jsonBody); - } else if (contentType.type === 'text/plain') { - err = new Error(`Server returned ${httpStatus} error: ${body}`); - } - } - - if (!err) { - err = new Error(`Server returned ${httpStatus} error`); - } - - err.httpStatus = httpStatus; - return err; -} -/** - * extract the Content-Type header from the response object, and - * parse it to a `{type, parameters}` object. - * - * returns null if no content-type header could be found. - * - * @param {XMLHttpRequest|http.IncomingMessage} response response object - * @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found - */ - - -function getResponseContentType(response) { - let contentType; - - if (response.getResponseHeader) { - // XMLHttpRequest provides getResponseHeader - contentType = response.getResponseHeader("Content-Type"); - } else if (response.headers) { - // request provides http.IncomingMessage which has a message.headers map - contentType = response.headers['content-type'] || null; - } - - if (!contentType) { - return null; - } - - try { - return (0, _contentType.parse)(contentType); - } catch (e) { - throw new Error(`Error parsing Content-Type '${contentType}': ${e}`); - } -} - -/** - * Construct a Matrix error. This is a JavaScript Error with additional - * information specific to the standard Matrix error response. - * @constructor - * @param {Object} errorJson The Matrix error JSON returned from the homeserver. - * @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN". - * @prop {string} name Same as MatrixError.errcode but with a default unknown string. - * @prop {string} message The Matrix 'error' value, e.g. "Missing token." - * @prop {Object} data The raw Matrix error JSON used to construct this object. - * @prop {number} httpStatus The numeric HTTP status code given - */ -class MatrixError extends Error { - // set by http-api - constructor(errorJson = {}) { - super(`MatrixError: ${errorJson.errcode}`); - - _defineProperty(this, "errcode", void 0); - - _defineProperty(this, "data", void 0); - - _defineProperty(this, "httpStatus", void 0); - - this.errcode = errorJson.errcode; - this.name = errorJson.errcode || "Unknown error code"; - this.message = errorJson.error || "Unknown message"; - this.data = errorJson; - } - -} -/** - * Construct a ConnectionError. This is a JavaScript Error indicating - * that a request failed because of some error with the connection, either - * CORS was not correctly configured on the server, the server didn't response, - * the request timed out, or the internet connection on the client side went down. - * @constructor - */ - - -exports.MatrixError = MatrixError; - -class ConnectionError extends Error { - constructor(message, cause = undefined) { - super(message + (cause ? `: ${cause.message}` : "")); - } - - get name() { - return "ConnectionError"; - } - -} - -exports.ConnectionError = ConnectionError; - -class AbortError extends Error { - constructor() { - super("Operation aborted"); - } - - get name() { - return "AbortError"; - } - -} -/** - * Retries a network operation run in a callback. - * @param {number} maxAttempts maximum attempts to try - * @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again. - * @return {any} the result of the network operation - * @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError - */ - - -exports.AbortError = AbortError; - -async function retryNetworkOperation(maxAttempts, callback) { - let attempts = 0; - let lastConnectionError = null; - - while (attempts < maxAttempts) { - try { - if (attempts > 0) { - const timeout = 1000 * Math.pow(2, attempts); - - _logger.logger.log(`network operation failed ${attempts} times,` + ` retrying in ${timeout}ms...`); - - await (0, utils.sleep)(timeout); - } - - return callback(); - } catch (err) { - if (err instanceof ConnectionError) { - attempts += 1; - lastConnectionError = err; - } else { - throw err; - } - } - } - - throw lastConnectionError; -} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/indexeddb-helpers.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/indexeddb-helpers.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/indexeddb-helpers.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/indexeddb-helpers.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,7 +4,6 @@ value: true }); exports.exists = exists; - /* Copyright 2019 New Vector Ltd @@ -25,27 +24,23 @@ * Check if an IndexedDB database exists. The only way to do so is to try opening it, so * we do that and then delete it did not exist before. * - * @param {Object} indexedDB The `indexedDB` interface - * @param {string} dbName The database name to test for - * @returns {boolean} Whether the database exists + * @param indexedDB - The `indexedDB` interface + * @param dbName - The database name to test for + * @returns Whether the database exists */ function exists(indexedDB, dbName) { return new Promise((resolve, reject) => { let exists = true; const req = indexedDB.open(dbName); - req.onupgradeneeded = () => { // Since we did not provide an explicit version when opening, this event // should only fire if the DB did not exist before at any version. exists = false; }; - req.onblocked = () => reject(req.error); - req.onsuccess = () => { const db = req.result; db.close(); - if (!exists) { // The DB did not exist before, but has been created as part of this // existence check. Delete it now to restore previous state. Delete can @@ -54,10 +49,8 @@ // properly set up the DB. indexedDB.deleteDatabase(dbName); } - resolve(exists); }; - - req.onerror = ev => reject(req.error); + req.onerror = () => reject(req.error); }); } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/indexeddb-worker.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/indexeddb-worker.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/indexeddb-worker.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/indexeddb-worker.js 2023-04-11 06:11:52.000000000 +0000 @@ -9,5 +9,4 @@ return _indexeddbStoreWorker.IndexedDBStoreWorker; } }); - var _indexeddbStoreWorker = require("./store/indexeddb-store-worker"); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/index.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,11 +5,7 @@ }); var _exportNames = {}; exports.default = void 0; - -var request = _interopRequireWildcard(require("request")); - var matrixcs = _interopRequireWildcard(require("./matrix")); - Object.keys(matrixcs).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -21,15 +17,8 @@ } }); }); - -var utils = _interopRequireWildcard(require("./utils")); - -var _logger = require("./logger"); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - /* Copyright 2019 The Matrix.org Foundation C.I.C. @@ -45,20 +34,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -if (matrixcs.getRequest()) { - throw new Error("Multiple matrix-js-sdk entrypoints detected!"); -} - -matrixcs.request(request); -try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const crypto = require('crypto'); - - utils.setCrypto(crypto); -} catch (err) { - _logger.logger.log('nodejs was compiled without crypto support'); +if (global.__js_sdk_entrypoint) { + throw new Error("Multiple matrix-js-sdk entrypoints detected!"); } - +global.__js_sdk_entrypoint = true; var _default = matrixcs; exports.default = _default; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/interactive-auth.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/interactive-auth.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/interactive-auth.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/interactive-auth.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,18 +4,15 @@ value: true }); exports.InteractiveAuth = exports.AuthType = void 0; - var _logger = require("./logger"); - var _utils = require("./utils"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const EMAIL_STAGE_TYPE = "m.login.email.identity"; const MSISDN_STAGE_TYPE = "m.login.msisdn"; let AuthType; exports.AuthType = AuthType; - (function (AuthType) { AuthType["Password"] = "m.login.password"; AuthType["Recaptcha"] = "m.login.recaptcha"; @@ -28,19 +25,15 @@ AuthType["RegistrationToken"] = "m.login.registration_token"; AuthType["UnstableRegistrationToken"] = "org.matrix.msc3231.login.registration_token"; })(AuthType || (exports.AuthType = AuthType = {})); - class NoAuthFlowFoundError extends Error { // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase constructor(m, required_stages, flows) { super(m); this.required_stages = required_stages; this.flows = flows; - _defineProperty(this, "name", "NoAuthFlowFoundError"); } - } - /** * Abstracts the logic used to drive the interactive auth process. * @@ -53,119 +46,39 @@ * callbacks, and information gathered from the user can be submitted with * submitAuthDict. * - * @constructor - * @alias module:interactive-auth - * - * @param {object} opts options object - * - * @param {object} opts.matrixClient A matrix client to use for the auth process - * - * @param {object?} opts.authData error response from the last request. If - * null, a request will be made with no auth before starting. - * - * @param {function(object?): Promise} opts.doRequest - * called with the new auth dict to submit the request. Also passes a - * second deprecated arg which is a flag set to true if this request - * is a background request. The busyChanged callback should be used - * instead of the background flag. Should return a promise which resolves - * to the successful response or rejects with a MatrixError. - * - * @param {function(boolean): Promise} opts.busyChanged - * called whenever the interactive auth logic becomes busy submitting - * information provided by the user or finishes. After this has been - * called with true the UI should indicate that a request is in progress - * until it is called again with false. - * - * @param {function(string, object?)} opts.stateUpdated - * called when the status of the UI auth changes, ie. when the state of - * an auth stage changes of when the auth flow moves to a new stage. - * The arguments are: the login type (eg m.login.password); and an object - * which is either an error or an informational object specific to the - * login type. If the 'errcode' key is defined, the object is an error, - * and has keys: - * errcode: string, the textual error code, eg. M_UNKNOWN - * error: string, human readable string describing the error - * - * The login type specific objects are as follows: - * m.login.email.identity: - * * emailSid: string, the sid of the active email auth session - * - * @param {object?} opts.inputs Inputs provided by the user and used by different - * stages of the auto process. The inputs provided will affect what flow is chosen. - * - * @param {string?} opts.inputs.emailAddress An email address. If supplied, a flow - * using email verification will be chosen. - * - * @param {string?} opts.inputs.phoneCountry An ISO two letter country code. Gives - * the country that opts.phoneNumber should be resolved relative to. - * - * @param {string?} opts.inputs.phoneNumber A phone number. If supplied, a flow - * using phone number validation will be chosen. - * - * @param {string?} opts.sessionId If resuming an existing interactive auth session, - * the sessionId of that session. - * - * @param {string?} opts.clientSecret If resuming an existing interactive auth session, - * the client secret for that session - * - * @param {string?} opts.emailSid If returning from having completed m.login.email.identity - * auth, the sid for the email verification session. - * - * @param {function?} opts.requestEmailToken A function that takes the email address (string), - * clientSecret (string), attempt number (int) and sessionId (string) and calls the - * relevant requestToken function and returns the promise returned by that function. - * If the resulting promise rejects, the rejection will propagate through to the - * attemptAuth promise. - * + * @param opts - options object */ class InteractiveAuth { // if we are currently trying to submit an auth dict (which includes polling) // the promise the will resolve/reject when it completes + constructor(opts) { _defineProperty(this, "matrixClient", void 0); - _defineProperty(this, "inputs", void 0); - _defineProperty(this, "clientSecret", void 0); - _defineProperty(this, "requestCallback", void 0); - _defineProperty(this, "busyChangedCallback", void 0); - _defineProperty(this, "stateUpdatedCallback", void 0); - _defineProperty(this, "requestEmailTokenCallback", void 0); - _defineProperty(this, "data", void 0); - _defineProperty(this, "emailSid", void 0); - _defineProperty(this, "requestingEmailToken", false); - _defineProperty(this, "attemptAuthDeferred", null); - _defineProperty(this, "chosenFlow", null); - _defineProperty(this, "currentStage", null); - _defineProperty(this, "emailAttempt", 1); - _defineProperty(this, "submitPromise", null); - _defineProperty(this, "requestEmailToken", async () => { if (!this.requestingEmailToken) { - _logger.logger.trace("Requesting email token. Attempt: " + this.emailAttempt); // If we've picked a flow with email auth, we send the email + _logger.logger.trace("Requesting email token. Attempt: " + this.emailAttempt); + // If we've picked a flow with email auth, we send the email // now because we want the request to fail as soon as possible // if the email address is not valid (ie. already taken or not // registered, depending on what the operation is). - - this.requestingEmailToken = true; - try { const requestTokenResult = await this.requestEmailTokenCallback(this.inputs.emailAddress, this.clientSecret, this.emailAttempt++, this.data.session); this.emailSid = requestTokenResult.sid; - _logger.logger.trace("Email token request succeeded"); } finally { this.requestingEmailToken = false; @@ -174,38 +87,37 @@ _logger.logger.warn("Could not request email token: Already requesting"); } }); - this.matrixClient = opts.matrixClient; this.data = opts.authData || {}; this.requestCallback = opts.doRequest; - this.busyChangedCallback = opts.busyChanged; // startAuthStage included for backwards compat - + this.busyChangedCallback = opts.busyChanged; + // startAuthStage included for backwards compat this.stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage; this.requestEmailTokenCallback = opts.requestEmailToken; this.inputs = opts.inputs || {}; if (opts.sessionId) this.data.session = opts.sessionId; this.clientSecret = opts.clientSecret || this.matrixClient.generateClientSecret(); - this.emailSid = opts.emailSid ?? null; + this.emailSid = opts.emailSid; } + /** * begin the authentication process. * - * @return {Promise} which resolves to the response on success, + * @returns which resolves to the response on success, * or rejects with the error on failure. Rejects with NoAuthFlowFoundError if * no suitable authentication flow can be found */ - - attemptAuth() { // This promise will be quite long-lived and will resolve when the // request is authenticated and completes successfully. - this.attemptAuthDeferred = (0, _utils.defer)(); // pluck the promise out now, as doRequest may clear before we return - - const promise = this.attemptAuthDeferred.promise; // if we have no flows, try a request to acquire the flows + this.attemptAuthDeferred = (0, _utils.defer)(); + // pluck the promise out now, as doRequest may clear before we return + const promise = this.attemptAuthDeferred.promise; + // if we have no flows, try a request to acquire the flows if (!this.data?.flows) { - this.busyChangedCallback?.(true); // use the existing sessionId, if one is present. - + this.busyChangedCallback?.(true); + // use the existing sessionId, if one is present. const auth = this.data.session ? { session: this.data.session } : null; @@ -215,25 +127,22 @@ } else { this.startNextAuthStage(); } - return promise; } + /** * Poll to check if the auth session or current stage has been * completed out-of-band. If so, the attemptAuth promise will * be resolved. */ - - async poll() { - if (!this.data.session) return; // likewise don't poll if there is no auth session in progress - - if (!this.attemptAuthDeferred) return; // if we currently have a request in flight, there's no point making + if (!this.data.session) return; + // likewise don't poll if there is no auth session in progress + if (!this.attemptAuthDeferred) return; + // if we currently have a request in flight, there's no point making // another just to check what the status is - if (this.submitPromise) return; let authDict = {}; - if (this.currentStage == EMAIL_STAGE_TYPE) { // The email can be validated out-of-band, but we need to provide the // creds so the HS can go & check it. @@ -242,12 +151,10 @@ sid: this.emailSid, client_secret: this.clientSecret }; - if (await this.matrixClient.doesServerRequireIdServerParam()) { const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl()); creds.id_server = idServerParsedUrl.host; } - authDict = { type: EMAIL_STAGE_TYPE, // TODO: Remove `threepid_creds` once servers support proper UIA @@ -258,82 +165,74 @@ }; } } - this.submitAuthDict(authDict, true); } + /** * get the auth session ID * - * @return {string} session id + * @returns session id */ - - getSessionId() { return this.data?.session; } + /** * get the client secret used for validation sessions * with the identity server. * - * @return {string} client secret + * @returns client secret */ - - getClientSecret() { return this.clientSecret; } + /** * get the server params for a given stage * - * @param {string} loginType login type for the stage - * @return {object?} any parameters from the server for this stage + * @param loginType - login type for the stage + * @returns any parameters from the server for this stage */ - - getStageParams(loginType) { return this.data.params?.[loginType]; } - getChosenFlow() { return this.chosenFlow; } + /** * submit a new auth dict and fire off the request. This will either * make attemptAuth resolve/reject, or cause the startAuthStage callback * to be called for a new stage. * - * @param {object} authData new auth dict to send to the server. Should + * @param authData - new auth dict to send to the server. Should * include a `type` property denoting the login type, as well as any * other params for that stage. - * @param {boolean} background If true, this request failing will not result + * @param background - If true, this request failing will not result * in the attemptAuth promise being rejected. This can be set to true * for requests that just poll to see if auth has been completed elsewhere. */ - - async submitAuthDict(authData, background = false) { if (!this.attemptAuthDeferred) { throw new Error("submitAuthDict() called before attemptAuth()"); } - if (!background) { this.busyChangedCallback?.(true); - } // if we're currently trying a request, wait for it to finish + } + + // if we're currently trying a request, wait for it to finish // as otherwise we can get multiple 200 responses which can mean // things like multiple logins for register requests. // (but discard any exceptions as we only care when its done, // not whether it worked or not) - - while (this.submitPromise) { try { await this.submitPromise; } catch (e) {} - } // use the sessionid from the last request, if one is present. - + } + // use the sessionid from the last request, if one is present. let auth; - if (this.data.session) { auth = { session: this.data.session @@ -342,7 +241,6 @@ } else { auth = authData; } - try { // NB. the 'background' flag is deprecated by the busyChanged // callback and is here for backwards compat @@ -350,48 +248,45 @@ await this.submitPromise; } finally { this.submitPromise = null; - if (!background) { this.busyChangedCallback?.(false); } } } + /** * Gets the sid for the email validation session * Specific to m.login.email.identity * - * @returns {string} The sid of the email auth session + * @returns The sid of the email auth session */ - - getEmailSid() { return this.emailSid; } + /** * Sets the sid for the email validation session * This must be set in order to successfully poll for completion * of the email validation. * Specific to m.login.email.identity * - * @param {string} sid The sid for the email validation session + * @param sid - The sid for the email validation session */ - - setEmailSid(sid) { this.emailSid = sid; } + /** * Requests a new email token and sets the email sid for the validation session */ - /** * Fire off a request, and either resolve the promise, or call * startAuthStage. * - * @private - * @param {object?} auth new auth dict, including session id - * @param {boolean?} background If true, this request is a background poll, so it + * @internal + * @param auth - new auth dict, including session id + * @param background - If true, this request is a background poll, so it * failing will not result in the attemptAuth promise being rejected. * This can be set to true for requests that just poll to see if auth has * been completed elsewhere. @@ -405,7 +300,6 @@ // sometimes UI auth errors don't come with flows const errorFlows = error.data?.flows ?? null; const haveFlows = this.data.flows || Boolean(errorFlows); - if (error.httpStatus !== 401 || !error.data || !haveFlows) { // doesn't look like an interactive-auth failure. if (!background) { @@ -417,24 +311,20 @@ _logger.logger.log("Background poll request failed doing UI auth: ignoring", error); } } - if (!error.data) { error.data = {}; - } // if the error didn't come with flows, completed flows or session ID, + } + // if the error didn't come with flows, completed flows or session ID, // copy over the ones we have. Synapse sometimes sends responses without // any UI auth data (eg. when polling for email validation, if the email // has not yet been validated). This appears to be a Synapse bug, which // we workaround here. - - if (!error.data.flows && !error.data.completed && !error.data.session) { error.data.flows = this.data.flows; error.data.completed = this.data.completed; error.data.session = this.data.session; } - this.data = error.data; - try { this.startNextAuthStage(); } catch (e) { @@ -442,10 +332,10 @@ this.attemptAuthDeferred = null; return; } - - if (!this.emailSid && this.chosenFlow.stages.includes(AuthType.Email)) { + if (!this.emailSid && this.chosenFlow?.stages.includes(AuthType.Email)) { try { - await this.requestEmailToken(); // NB. promise is not resolved here - at some point, doRequest + await this.requestEmailToken(); + // NB. promise is not resolved here - at some point, doRequest // will be called again and if the user has jumped through all // the hoops correctly, auth will be complete and the request // will succeed. @@ -464,30 +354,25 @@ } } } + /** * Pick the next stage and call the callback * - * @private - * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + * @internal + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ - - startNextAuthStage() { const nextStage = this.chooseStage(); - if (!nextStage) { throw new Error("No incomplete flows from the server"); } - this.currentStage = nextStage; - if (nextStage === AuthType.Dummy) { this.submitAuthDict({ - type: 'm.login.dummy' + type: "m.login.dummy" }); return; } - if (this.data?.errcode || this.data?.error) { this.stateUpdatedCallback(nextStage, { errcode: this.data?.errcode || "", @@ -495,33 +380,28 @@ }); return; } - this.stateUpdatedCallback(nextStage, nextStage === EMAIL_STAGE_TYPE ? { emailSid: this.emailSid } : {}); } + /** * Pick the next auth stage * - * @private - * @return {string?} login type - * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + * @internal + * @returns login type + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ - - chooseStage() { if (this.chosenFlow === null) { this.chosenFlow = this.chooseFlow(); } - _logger.logger.log("Active flow => %s", JSON.stringify(this.chosenFlow)); - const nextStage = this.firstUncompletedStage(this.chosenFlow); - _logger.logger.log("Next stage: %s", nextStage); - return nextStage; } + /** * Pick one of the flows from the returned list * If a flow using all of the inputs is found, it will @@ -533,22 +413,19 @@ * this could result in the email not being used which would leave * the account with no means to reset a password. * - * @private - * @return {object} flow - * @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found + * @internal + * @returns flow + * @throws {@link NoAuthFlowFoundError} If no suitable authentication flow can be found */ - - chooseFlow() { - const flows = this.data.flows || []; // we've been given an email or we've already done an email part + const flows = this.data.flows || []; + // we've been given an email or we've already done an email part const haveEmail = Boolean(this.inputs.emailAddress) || Boolean(this.emailSid); const haveMsisdn = Boolean(this.inputs.phoneCountry) && Boolean(this.inputs.phoneNumber); - for (const flow of flows) { let flowHasEmail = false; let flowHasMsisdn = false; - for (const stage of flow.stages) { if (stage === EMAIL_STAGE_TYPE) { flowHasEmail = true; @@ -556,40 +433,27 @@ flowHasMsisdn = true; } } - if (flowHasEmail == haveEmail && flowHasMsisdn == haveMsisdn) { return flow; } } - const requiredStages = []; if (haveEmail) requiredStages.push(EMAIL_STAGE_TYPE); - if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE); // Throw an error with a fairly generic description, but with more + if (haveMsisdn) requiredStages.push(MSISDN_STAGE_TYPE); + // Throw an error with a fairly generic description, but with more // information such that the app can give a better one if so desired. - throw new NoAuthFlowFoundError("No appropriate authentication flow found", requiredStages, flows); } + /** * Get the first uncompleted stage in the given flow * - * @private - * @param {object} flow - * @return {string} login type + * @internal + * @returns login type */ - - firstUncompletedStage(flow) { const completed = this.data.completed || []; - - for (let i = 0; i < flow.stages.length; ++i) { - const stageType = flow.stages[i]; - - if (completed.indexOf(stageType) === -1) { - return stageType; - } - } + return flow.stages.find(stageType => !completed.includes(stageType)); } - } - exports.InteractiveAuth = InteractiveAuth; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/logger.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/logger.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/logger.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/logger.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,11 +4,8 @@ value: true }); exports.logger = void 0; - var _loglevel = _interopRequireDefault(require("loglevel")); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - /* Copyright 2018 André Jaenisch Copyright 2019, 2021 The Matrix.org Foundation C.I.C. @@ -26,21 +23,19 @@ limitations under the License. */ -/** - * @module logger - */ // This is to demonstrate, that you can use any namespace you want. // Namespaces allow you to turn on/off the logging for specific parts of the // application. // An idea would be to control this via an environment variable (on Node.js). // See https://www.npmjs.com/package/debug to see how this could be implemented // Part of #332 is introducing a logging library in the first place. -const DEFAULT_NAMESPACE = "matrix"; // because rageshakes in react-sdk hijack the console log, also at module load time, +const DEFAULT_NAMESPACE = "matrix"; + +// because rageshakes in react-sdk hijack the console log, also at module load time, // initializing the logger here races with the initialization of rageshakes. // to avoid the issue, we override the methodFactory of loglevel that binds to the // console methods at initialization time by a factory that looks up the console methods // when logging so we always get the current value of console methods. - _loglevel.default.methodFactory = function (methodName, logLevel, loggerName) { return function (...args) { /* eslint-disable @typescript-eslint/no-invalid-this */ @@ -48,49 +43,38 @@ args.unshift(this.prefix); } /* eslint-enable @typescript-eslint/no-invalid-this */ - - const supportedByConsole = methodName === "error" || methodName === "warn" || methodName === "trace" || methodName === "info"; /* eslint-disable no-console */ - if (supportedByConsole) { return console[methodName](...args); } else { return console.log(...args); } /* eslint-enable no-console */ - }; }; + /** - * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. + * Drop-in replacement for `console` using {@link https://www.npmjs.com/package/loglevel|loglevel}. * Can be tailored down to specific use cases if needed. */ - - const logger = _loglevel.default.getLogger(DEFAULT_NAMESPACE); - exports.logger = logger; logger.setLevel(_loglevel.default.levels.DEBUG, false); - function extendLogger(logger) { logger.withPrefix = function (prefix) { const existingPrefix = this.prefix || ""; return getPrefixedLogger(existingPrefix + prefix); }; } - extendLogger(logger); - function getPrefixedLogger(prefix) { const prefixLogger = _loglevel.default.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`); - if (prefixLogger.prefix !== prefix) { // Only do this setup work the first time through, as loggers are saved by name. extendLogger(prefixLogger); prefixLogger.prefix = prefix; prefixLogger.setLevel(_loglevel.default.levels.DEBUG, false); } - return prefixLogger; } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/matrix.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/matrix.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/matrix.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/matrix.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,15 +4,41 @@ value: true }); var _exportNames = { - request: true, - getRequest: true, - wrapRequest: true, setCryptoStoreFactory: true, createClient: true, + createRoomWidgetClient: true, ContentHelpers: true, - createNewMatrixCall: true + createNewMatrixCall: true, + GroupCallEvent: true, + GroupCallIntent: true, + GroupCallState: true, + GroupCallType: true }; exports.ContentHelpers = void 0; +Object.defineProperty(exports, "GroupCallEvent", { + enumerable: true, + get: function () { + return _groupCall.GroupCallEvent; + } +}); +Object.defineProperty(exports, "GroupCallIntent", { + enumerable: true, + get: function () { + return _groupCall.GroupCallIntent; + } +}); +Object.defineProperty(exports, "GroupCallState", { + enumerable: true, + get: function () { + return _groupCall.GroupCallState; + } +}); +Object.defineProperty(exports, "GroupCallType", { + enumerable: true, + get: function () { + return _groupCall.GroupCallType; + } +}); exports.createClient = createClient; Object.defineProperty(exports, "createNewMatrixCall", { enumerable: true, @@ -20,13 +46,9 @@ return _call.createNewMatrixCall; } }); -exports.getRequest = getRequest; -exports.request = request; +exports.createRoomWidgetClient = createRoomWidgetClient; exports.setCryptoStoreFactory = setCryptoStoreFactory; -exports.wrapRequest = wrapRequest; - var _memoryCryptoStore = require("./crypto/store/memory-crypto-store"); - Object.keys(_memoryCryptoStore).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -38,9 +60,7 @@ } }); }); - var _memory = require("./store/memory"); - Object.keys(_memory).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -52,9 +72,7 @@ } }); }); - var _scheduler = require("./scheduler"); - Object.keys(_scheduler).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -66,9 +84,7 @@ } }); }); - var _client = require("./client"); - Object.keys(_client).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -80,9 +96,19 @@ } }); }); - +var _embedded = require("./embedded"); +Object.keys(_embedded).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _embedded[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _embedded[key]; + } + }); +}); var _httpApi = require("./http-api"); - Object.keys(_httpApi).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -94,9 +120,7 @@ } }); }); - var _autodiscovery = require("./autodiscovery"); - Object.keys(_autodiscovery).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -108,9 +132,7 @@ } }); }); - var _syncAccumulator = require("./sync-accumulator"); - Object.keys(_syncAccumulator).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -122,9 +144,7 @@ } }); }); - var _errors = require("./errors"); - Object.keys(_errors).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -136,9 +156,7 @@ } }); }); - var _beacon = require("./models/beacon"); - Object.keys(_beacon).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -150,9 +168,7 @@ } }); }); - var _event = require("./models/event"); - Object.keys(_event).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -164,9 +180,7 @@ } }); }); - var _room = require("./models/room"); - Object.keys(_room).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -178,9 +192,7 @@ } }); }); - var _eventTimeline = require("./models/event-timeline"); - Object.keys(_eventTimeline).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -192,9 +204,7 @@ } }); }); - var _eventTimelineSet = require("./models/event-timeline-set"); - Object.keys(_eventTimelineSet).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -206,9 +216,19 @@ } }); }); - +var _poll = require("./models/poll"); +Object.keys(_poll).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _poll[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _poll[key]; + } + }); +}); var _roomMember = require("./models/room-member"); - Object.keys(_roomMember).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -220,9 +240,7 @@ } }); }); - var _roomState = require("./models/room-state"); - Object.keys(_roomState).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -234,9 +252,7 @@ } }); }); - var _user = require("./models/user"); - Object.keys(_user).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -248,9 +264,7 @@ } }); }); - var _filter = require("./filter"); - Object.keys(_filter).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -262,9 +276,7 @@ } }); }); - var _timelineWindow = require("./timeline-window"); - Object.keys(_timelineWindow).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -276,9 +288,7 @@ } }); }); - var _interactiveAuth = require("./interactive-auth"); - Object.keys(_interactiveAuth).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -290,9 +300,7 @@ } }); }); - var _serviceTypes = require("./service-types"); - Object.keys(_serviceTypes).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -304,9 +312,7 @@ } }); }); - var _indexeddb = require("./store/indexeddb"); - Object.keys(_indexeddb).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -318,9 +324,7 @@ } }); }); - var _indexeddbCryptoStore = require("./crypto/store/indexeddb-crypto-store"); - Object.keys(_indexeddbCryptoStore).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -332,9 +336,7 @@ } }); }); - var _contentRepo = require("./content-repo"); - Object.keys(_contentRepo).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -346,9 +348,7 @@ } }); }); - var _event2 = require("./@types/event"); - Object.keys(_event2).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -360,9 +360,7 @@ } }); }); - var _PushRules = require("./@types/PushRules"); - Object.keys(_PushRules).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -374,9 +372,7 @@ } }); }); - var _partials = require("./@types/partials"); - Object.keys(_partials).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -388,9 +384,7 @@ } }); }); - var _requests = require("./@types/requests"); - Object.keys(_requests).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -402,9 +396,7 @@ } }); }); - var _search = require("./@types/search"); - Object.keys(_search).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -416,9 +408,7 @@ } }); }); - var _roomSummary = require("./models/room-summary"); - Object.keys(_roomSummary).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; @@ -430,19 +420,14 @@ } }); }); - var _ContentHelpers = _interopRequireWildcard(require("./content-helpers")); - exports.ContentHelpers = _ContentHelpers; - var _call = require("./webrtc/call"); - +var _groupCall = require("./webrtc/groupCall"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - /* -Copyright 2015-2021 The Matrix.org Foundation C.I.C. +Copyright 2015-2022 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -456,130 +441,41 @@ See the License for the specific language governing permissions and limitations under the License. */ -// expose the underlying request object so different environments can use -// different request libs (e.g. request or browser-request) -let requestInstance; -/** - * The function used to perform HTTP requests. Only use this if you want to - * use a different HTTP library, e.g. Angular's $http. This should - * be set prior to calling {@link createClient}. - * @param {requestFunction} r The request function to use. - */ - -function request(r) { - requestInstance = r; -} -/** - * Return the currently-set request function. - * @return {requestFunction} The current request function. - */ - - -function getRequest() { - return requestInstance; -} -/** - * Apply wrapping code around the request function. The wrapper function is - * installed as the new request handler, and when invoked it is passed the - * previous value, along with the options and callback arguments. - * @param {requestWrapperFunction} wrapper The wrapping function. - */ - -function wrapRequest(wrapper) { - const origRequest = requestInstance; - - requestInstance = function (options, callback) { - return wrapper(origRequest, options, callback); - }; -} +// used to be located here let cryptoStoreFactory = () => new _memoryCryptoStore.MemoryCryptoStore(); + /** * Configure a different factory to be used for creating crypto stores * - * @param {Function} fac a function which will return a new - * {@link module:crypto.store.base~CryptoStore}. + * @param fac - a function which will return a new {@link CryptoStore} */ - - function setCryptoStoreFactory(fac) { cryptoStoreFactory = fac; } +function amendClientOpts(opts) { + opts.store = opts.store ?? new _memory.MemoryStore({ + localStorage: global.localStorage + }); + opts.scheduler = opts.scheduler ?? new _scheduler.MatrixScheduler(); + opts.cryptoStore = opts.cryptoStore ?? cryptoStoreFactory(); + return opts; +} /** - * Construct a Matrix Client. Similar to {@link module:client.MatrixClient} + * Construct a Matrix Client. Similar to {@link MatrixClient} * except that the 'request', 'store' and 'scheduler' dependencies are satisfied. - * @param {(Object|string)} opts The configuration options for this client. If - * this is a string, it is assumed to be the base URL. These configuration - * options will be passed directly to {@link module:client.MatrixClient}. - * @param {Object} opts.store If not set, defaults to - * {@link module:store/memory.MemoryStore}. - * @param {Object} opts.scheduler If not set, defaults to - * {@link module:scheduler~MatrixScheduler}. - * @param {requestFunction} opts.request If not set, defaults to the function - * supplied to {@link request} which defaults to the request module from NPM. + * @param opts - The configuration options for this client. These configuration + * options will be passed directly to {@link MatrixClient}. * - * @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore - * crypto store implementation. Calls the factory supplied to - * {@link setCryptoStoreFactory} if unspecified; or if no factory has been - * specified, uses a default implementation (indexeddb in the browser, - * in-memory otherwise). - * - * @return {MatrixClient} A new matrix client. - * @see {@link module:client.MatrixClient} for the full list of options for - * opts. + * @returns A new matrix client. + * @see {@link MatrixClient} for the full list of options for + * `opts`. */ function createClient(opts) { - if (typeof opts === "string") { - opts = { - "baseUrl": opts - }; - } - - opts.request = opts.request || requestInstance; - opts.store = opts.store || new _memory.MemoryStore({ - localStorage: global.localStorage - }); - opts.scheduler = opts.scheduler || new _scheduler.MatrixScheduler(); - opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory(); - return new _client.MatrixClient(opts); + return new _client.MatrixClient(amendClientOpts(opts)); } -/** - * The request function interface for performing HTTP requests. This matches the - * API for the {@link https://github.com/request/request#requestoptions-callback| - * request NPM module}. The SDK will attempt to call this function in order to - * perform an HTTP request. - * @callback requestFunction - * @param {Object} opts The options for this HTTP request. - * @param {string} opts.uri The complete URI. - * @param {string} opts.method The HTTP method. - * @param {Object} opts.qs The query parameters to append to the URI. - * @param {Object} opts.body The JSON-serializable object. - * @param {boolean} opts.json True if this is a JSON request. - * @param {Object} opts._matrix_opts The underlying options set for - * {@link MatrixHttpApi}. - * @param {requestCallback} callback The request callback. - */ - -/** - * A wrapper for the request function interface. - * @callback requestWrapperFunction - * @param {requestFunction} origRequest The underlying request function being - * wrapped - * @param {Object} opts The options for this HTTP request, given in the same - * form as {@link requestFunction}. - * @param {requestCallback} callback The request callback. - */ - -/** - * The request callback interface for performing HTTP requests. This matches the - * API for the {@link https://github.com/request/request#requestoptions-callback| - * request NPM module}. The SDK will implement a callback which meets this - * interface in order to handle the HTTP response. - * @callback requestCallback - * @param {Error} err The error if one occurred, else falsey. - * @param {Object} response The HTTP response which consists of - * {statusCode: {Number}, headers: {Object}} - * @param {Object} body The parsed HTTP response body. - */ \ No newline at end of file +function createRoomWidgetClient(widgetApi, capabilities, roomId, opts) { + return new _embedded.RoomWidgetClient(widgetApi, capabilities, roomId, amendClientOpts(opts)); +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/beacon.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/beacon.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/beacon.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/beacon.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,18 +4,14 @@ value: true }); exports.isTimestampInDuration = exports.getBeaconInfoIdentifier = exports.BeaconEvent = exports.Beacon = void 0; - var _contentHelpers = require("../content-helpers"); - var _utils = require("../utils"); - var _typedEventEmitter = require("./typed-event-emitter"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } let BeaconEvent; exports.BeaconEvent = BeaconEvent; - (function (BeaconEvent) { BeaconEvent["New"] = "Beacon.new"; BeaconEvent["Update"] = "Beacon.update"; @@ -23,181 +19,147 @@ BeaconEvent["Destroy"] = "Beacon.Destroy"; BeaconEvent["LocationUpdate"] = "Beacon.LocationUpdate"; })(BeaconEvent || (exports.BeaconEvent = BeaconEvent = {})); +const isTimestampInDuration = (startTimestamp, durationMs, timestamp) => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp; -const isTimestampInDuration = (startTimestamp, durationMs, timestamp) => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp; // beacon info events are uniquely identified by +// beacon info events are uniquely identified by // `_` - - exports.isTimestampInDuration = isTimestampInDuration; +const getBeaconInfoIdentifier = event => `${event.getRoomId()}_${event.getStateKey()}`; -const getBeaconInfoIdentifier = event => `${event.getRoomId()}_${event.getStateKey()}`; // https://github.com/matrix-org/matrix-spec-proposals/pull/3672 - - +// https://github.com/matrix-org/matrix-spec-proposals/pull/3672 exports.getBeaconInfoIdentifier = getBeaconInfoIdentifier; - class Beacon extends _typedEventEmitter.TypedEventEmitter { constructor(rootEvent) { super(); this.rootEvent = rootEvent; - _defineProperty(this, "roomId", void 0); - _defineProperty(this, "_beaconInfo", void 0); - _defineProperty(this, "_isLive", void 0); - _defineProperty(this, "livenessWatchTimeout", void 0); - _defineProperty(this, "_latestLocationEvent", void 0); - _defineProperty(this, "clearLatestLocation", () => { this._latestLocationEvent = undefined; this.emit(BeaconEvent.LocationUpdate, this.latestLocationState); }); - this.setBeaconInfo(this.rootEvent); this.roomId = this.rootEvent.getRoomId(); } - get isLive() { - return this._isLive; + return !!this._isLive; } - get identifier() { return getBeaconInfoIdentifier(this.rootEvent); } - get beaconInfoId() { return this.rootEvent.getId(); } - get beaconInfoOwner() { return this.rootEvent.getStateKey(); } - get beaconInfoEventType() { return this.rootEvent.getType(); } - get beaconInfo() { return this._beaconInfo; } - get latestLocationState() { return this._latestLocationEvent && (0, _contentHelpers.parseBeaconContent)(this._latestLocationEvent.getContent()); } - get latestLocationEvent() { return this._latestLocationEvent; } - update(beaconInfoEvent) { if (getBeaconInfoIdentifier(beaconInfoEvent) !== this.identifier) { - throw new Error('Invalid updating event'); - } // don't update beacon with an older event - - - if (beaconInfoEvent.event.origin_server_ts < this.rootEvent.event.origin_server_ts) { + throw new Error("Invalid updating event"); + } + // don't update beacon with an older event + if (beaconInfoEvent.getTs() < this.rootEvent.getTs()) { return; } - this.rootEvent = beaconInfoEvent; this.setBeaconInfo(this.rootEvent); this.emit(BeaconEvent.Update, beaconInfoEvent, this); this.clearLatestLocation(); } - destroy() { if (this.livenessWatchTimeout) { clearTimeout(this.livenessWatchTimeout); } - this._isLive = false; this.emit(BeaconEvent.Destroy, this.identifier); } + /** * Monitor liveness of a beacon * Emits BeaconEvent.LivenessChange when beacon expires */ - - monitorLiveness() { if (this.livenessWatchTimeout) { clearTimeout(this.livenessWatchTimeout); } - this.checkLiveness(); - + if (!this.beaconInfo) return; if (this.isLive) { - const expiryInMs = this._beaconInfo?.timestamp + this._beaconInfo?.timeout - Date.now(); - + const expiryInMs = this.beaconInfo.timestamp + this.beaconInfo.timeout - Date.now(); if (expiryInMs > 1) { this.livenessWatchTimeout = setTimeout(() => { this.monitorLiveness(); }, expiryInMs); } - } else if (this._beaconInfo?.timestamp > Date.now()) { + } else if (this.beaconInfo.timestamp > Date.now()) { // beacon start timestamp is in the future // check liveness again then this.livenessWatchTimeout = setTimeout(() => { this.monitorLiveness(); - }, this.beaconInfo?.timestamp - Date.now()); + }, this.beaconInfo.timestamp - Date.now()); } } + /** * Process Beacon locations * Emits BeaconEvent.LocationUpdate */ - - addLocations(beaconLocationEvents) { // discard locations for beacons that are not live if (!this.isLive) { return; } - const validLocationEvents = beaconLocationEvents.filter(event => { const content = event.getContent(); const parsed = (0, _contentHelpers.parseBeaconContent)(content); if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these - const { timestamp } = parsed; - return (// only include positions that were taken inside the beacon's live period - isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) && ( // ignore positions older than our current latest location - !this.latestLocationState || timestamp > this.latestLocationState.timestamp) - ); + return this._beaconInfo.timestamp && + // only include positions that were taken inside the beacon's live period + isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) && ( + // ignore positions older than our current latest location + !this.latestLocationState || timestamp > this.latestLocationState.timestamp); }); const latestLocationEvent = validLocationEvents.sort(_utils.sortEventsByLatestContentTimestamp)?.[0]; - if (latestLocationEvent) { this._latestLocationEvent = latestLocationEvent; this.emit(BeaconEvent.LocationUpdate, this.latestLocationState); } } - setBeaconInfo(event) { this._beaconInfo = (0, _contentHelpers.parseBeaconInfoContent)(event.getContent()); this.checkLiveness(); } - checkLiveness() { - const prevLiveness = this.isLive; // element web sets a beacon's start timestamp to the senders local current time + const prevLiveness = this.isLive; + + // element web sets a beacon's start timestamp to the senders local current time // when Alice's system clock deviates slightly from Bob's a beacon Alice intended to be live // may have a start timestamp in the future from Bob's POV // handle this by adding 6min of leniency to the start timestamp when it is in the future - - const startTimestamp = this._beaconInfo?.timestamp > Date.now() ? this._beaconInfo?.timestamp - 360000 - /* 6min */ - : this._beaconInfo?.timestamp; - this._isLive = !!this._beaconInfo?.live && isTimestampInDuration(startTimestamp, this._beaconInfo?.timeout, Date.now()); - + if (!this.beaconInfo) return; + const startTimestamp = this.beaconInfo.timestamp > Date.now() ? this.beaconInfo.timestamp - 360000 /* 6min */ : this.beaconInfo.timestamp; + this._isLive = !!this._beaconInfo?.live && !!startTimestamp && isTimestampInDuration(startTimestamp, this._beaconInfo?.timeout, Date.now()); if (prevLiveness !== this.isLive) { this.emit(BeaconEvent.LivenessChange, this.isLive, this); } } - } - exports.Beacon = Beacon; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-context.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-context.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-context.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-context.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,14 +4,10 @@ value: true }); exports.EventContext = void 0; - var _eventTimeline = require("./event-timeline"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -/** - * @module models/event-context - */ +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } class EventContext { /** * Construct a new EventContext @@ -23,93 +19,78 @@ * It also stores pagination tokens for going backwards and forwards in the * timeline. * - * @param {MatrixEvent} ourEvent the event at the centre of this context - * - * @constructor + * @param ourEvent - the event at the centre of this context */ constructor(ourEvent) { this.ourEvent = ourEvent; - _defineProperty(this, "timeline", void 0); - _defineProperty(this, "ourEventIndex", 0); - _defineProperty(this, "paginateTokens", { [_eventTimeline.Direction.Backward]: null, [_eventTimeline.Direction.Forward]: null }); - this.timeline = [ourEvent]; } + /** * Get the main event of interest * * This is a convenience function for getTimeline()[getOurEventIndex()]. * - * @return {MatrixEvent} The event at the centre of this context. + * @returns The event at the centre of this context. */ - - getEvent() { return this.timeline[this.ourEventIndex]; } + /** * Get the list of events in this context * - * @return {Array} An array of MatrixEvents + * @returns An array of MatrixEvents */ - - getTimeline() { return this.timeline; } + /** * Get the index in the timeline of our event - * - * @return {Number} */ - - getOurEventIndex() { return this.ourEventIndex; } + /** * Get a pagination token. * - * @param {boolean} backwards true to get the pagination token for going - * backwards in time - * @return {string} + * @param backwards - true to get the pagination token for going */ - - getPaginateToken(backwards = false) { return this.paginateTokens[backwards ? _eventTimeline.Direction.Backward : _eventTimeline.Direction.Forward]; } + /** * Set a pagination token. * * Generally this will be used only by the matrix js sdk. * - * @param {string} token pagination token - * @param {boolean} backwards true to set the pagination token for going + * @param token - pagination token + * @param backwards - true to set the pagination token for going * backwards in time */ - - setPaginateToken(token, backwards = false) { - this.paginateTokens[backwards ? _eventTimeline.Direction.Backward : _eventTimeline.Direction.Forward] = token; + this.paginateTokens[backwards ? _eventTimeline.Direction.Backward : _eventTimeline.Direction.Forward] = token ?? null; } + /** * Add more events to the timeline * - * @param {Array} events new events, in timeline order - * @param {boolean} atStart true to insert new events at the start + * @param events - new events, in timeline order + * @param atStart - true to insert new events at the start */ - - addEvents(events, atStart = false) { // TODO: should we share logic with Room.addEventsToTimeline? // Should Room even use EventContext? + if (atStart) { this.timeline = events.concat(this.timeline); this.ourEventIndex += events.length; @@ -117,7 +98,5 @@ this.timeline = this.timeline.concat(events); } } - } - exports.EventContext = EventContext; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event.js 2023-04-11 06:11:52.000000000 +0000 @@ -10,32 +10,25 @@ } }); exports.MatrixEventEvent = exports.MatrixEvent = void 0; - var _matrixEventsSdk = require("matrix-events-sdk"); - var _logger = require("../logger"); - var _event = require("../@types/event"); - var _utils = require("../utils"); - var _thread = require("./thread"); - var _ReEmitter = require("../ReEmitter"); - var _typedEventEmitter = require("./typed-event-emitter"); - +var _algorithms = require("../crypto/algorithms"); +var _OlmDevice = require("../crypto/OlmDevice"); var _eventStatus = require("./event-status"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // A singleton implementing `IMessageVisibilityVisible`. const MESSAGE_VISIBLE = Object.freeze({ visible: true }); let MatrixEventEvent; exports.MatrixEventEvent = MatrixEventEvent; - (function (MatrixEventEvent) { MatrixEventEvent["Decrypted"] = "Event.decrypted"; MatrixEventEvent["BeforeRedaction"] = "Event.beforeRedaction"; @@ -45,11 +38,11 @@ MatrixEventEvent["Replaced"] = "Event.replaced"; MatrixEventEvent["RelationsCreated"] = "Event.relationsCreated"; })(MatrixEventEvent || (exports.MatrixEventEvent = MatrixEventEvent = {})); - class MatrixEvent extends _typedEventEmitter.TypedEventEmitter { /* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531. Note: We're returning this object, so any value stored here MUST be frozen. */ + // Not all events will be extensible-event compatible, so cache a flag in // addition to a falsy cached event value. We check the flag later on in // a public getter to decide if the cache is valid. @@ -84,18 +77,57 @@ */ /** - * @experimental * A reference to the thread this event belongs to */ + /* + * True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and + * the sender has disabled encrypting to unverified devices. + */ + /* Set an approximate timestamp for the event relative the local clock. * This will inherently be approximate because it doesn't take into account * the time between the server putting the 'age' field on the event as it sent * it to us and the time we're now constructing this event, but that's better * than assuming the local clock is in sync with the origin HS's clock. */ - // XXX: these should be read-only - // only state events may be backwards looking + + /** + * The room member who sent this event, or null e.g. + * this is a presence event. This is only guaranteed to be set for events that + * appear in a timeline, ie. do not guarantee that it will be set on state + * events. + * @privateRemarks + * Should be read-only + */ + + /** + * The room member who is the target of this event, e.g. + * the invitee, the person being banned, etc. + * @privateRemarks + * Should be read-only + */ + + /** + * The sending status of the event. + * @privateRemarks + * Should be read-only + */ + + /** + * most recent error associated with sending the event, if any + * @privateRemarks + * Should be read-only + */ + + /** + * True if this event is 'forward looking', meaning + * that getDirectionalContent() will return event.content and not event.prev_content. + * Only state events may be backwards looking + * Default: true. This property is experimental and may change. + * @privateRemarks + * Should be read-only + */ /* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event, * `Crypto` will set this the `VerificationRequest` for the event @@ -104,87 +136,48 @@ /** * Construct a Matrix Event object - * @constructor - * - * @param {Object} event The raw event to be wrapped in this DAO * - * @prop {Object} event The raw (possibly encrypted) event. Do not access + * @param event - The raw (possibly encrypted) event. Do not access * this property directly unless you absolutely have to. Prefer the getter * methods defined on this class. Using the getter methods shields your app * from changes to event JSON between Matrix versions. - * - * @prop {RoomMember} sender The room member who sent this event, or null e.g. - * this is a presence event. This is only guaranteed to be set for events that - * appear in a timeline, ie. do not guarantee that it will be set on state - * events. - * @prop {RoomMember} target The room member who is the target of this event, e.g. - * the invitee, the person being banned, etc. - * @prop {EventStatus} status The sending status of the event. - * @prop {Error} error most recent error associated with sending the event, if any - * @prop {boolean} forwardLooking True if this event is 'forward looking', meaning - * that getDirectionalContent() will return event.content and not event.prev_content. - * Default: true. This property is experimental and may change. */ constructor(event = {}) { - super(); // intern the values of matrix events to force share strings and reduce the + super(); + + // intern the values of matrix events to force share strings and reduce the // amount of needless string duplication. This can save moderate amounts of // memory (~10% on a 350MB heap). // 'membership' at the event level (rather than the content level) is a legacy // field that Element never otherwise looks at, but it will still take up a lot // of space if we don't intern it. - this.event = event; - _defineProperty(this, "pushActions", null); - _defineProperty(this, "_replacingEvent", null); - _defineProperty(this, "_localRedactionEvent", null); - _defineProperty(this, "_isCancelled", false); - _defineProperty(this, "clearEvent", void 0); - _defineProperty(this, "visibility", MESSAGE_VISIBLE); - _defineProperty(this, "_hasCachedExtEv", false); - _defineProperty(this, "_cachedExtEv", undefined); - _defineProperty(this, "senderCurve25519Key", null); - _defineProperty(this, "claimedEd25519Key", null); - _defineProperty(this, "forwardingCurve25519KeyChain", []); - _defineProperty(this, "untrusted", null); - - _defineProperty(this, "_decryptionPromise", null); - + _defineProperty(this, "decryptionPromise", null); _defineProperty(this, "retryDecryption", false); - - _defineProperty(this, "txnId", null); - - _defineProperty(this, "thread", null); - + _defineProperty(this, "txnId", void 0); + _defineProperty(this, "thread", void 0); _defineProperty(this, "threadId", void 0); - + _defineProperty(this, "encryptedDisabledForUnverifiedDevices", false); _defineProperty(this, "localTimestamp", void 0); - _defineProperty(this, "sender", null); - _defineProperty(this, "target", null); - _defineProperty(this, "status", null); - _defineProperty(this, "error", null); - _defineProperty(this, "forwardLooking", true); - - _defineProperty(this, "verificationRequest", null); - + _defineProperty(this, "verificationRequest", void 0); _defineProperty(this, "reEmitter", void 0); - ["state_key", "type", "sender", "room_id", "membership"].forEach(prop => { if (typeof event[prop] !== "string") return; event[prop] = (0, _utils.internaliseString)(event[prop]); @@ -197,10 +190,11 @@ if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return; event.content["m.relates_to"][prop] = (0, _utils.internaliseString)(event.content["m.relates_to"][prop]); }); - this.txnId = event.txn_id || null; + this.txnId = event.txn_id; this.localTimestamp = Date.now() - (this.getAge() ?? 0); this.reEmitter = new _ReEmitter.TypedReEmitter(this); } + /** * Unstable getter to try and get an extensible event. Note that this might * return a falsy value if the event could not be parsed as an extensible @@ -208,27 +202,22 @@ * * @deprecated Use stable functions where possible. */ - - get unstableExtensibleEvent() { if (!this._hasCachedExtEv) { this._cachedExtEv = _matrixEventsSdk.ExtensibleEvents.parse(this.getEffectiveEvent()); } - return this._cachedExtEv; } - invalidateExtensibleEvent() { // just reset the flag - that'll trick the getter into parsing a new event this._hasCachedExtEv = false; } + /** * Gets the event as though it would appear unencrypted. If the event is already not * encrypted, it is simply returned as-is. - * @returns {IEvent} The event in wire format. + * @returns The event in wire format. */ - - getEffectiveEvent() { const content = Object.assign({}, this.getContent()); // clone for mutation @@ -244,118 +233,129 @@ if (["algorithm", "ciphertext", "device_id", "sender_key", "session_id"].includes(key)) { continue; } - if (content[key] === undefined) content[key] = value; } - } // clearEvent doesn't have all the fields, so we'll copy what we can from this.event. - // We also copy over our "fixed" content key. - + } + // clearEvent doesn't have all the fields, so we'll copy what we can from this.event. + // We also copy over our "fixed" content key. return Object.assign({}, this.event, this.clearEvent, { content }); } + /** * Get the event_id for this event. - * @return {string} The event ID, e.g. $143350589368169JsLZx:localhost + * @returns The event ID, e.g. $143350589368169JsLZx:localhost * */ - - getId() { return this.event.event_id; } + /** * Get the user_id for this event. - * @return {string} The user ID, e.g. @alice:matrix.org + * @returns The user ID, e.g. `@alice:matrix.org` */ - - getSender() { return this.event.sender || this.event.user_id; // v2 / v1 } + /** * Get the (decrypted, if necessary) type of event. * - * @return {string} The event type, e.g. m.room.message + * @returns The event type, e.g. `m.room.message` */ - - getType() { if (this.clearEvent) { return this.clearEvent.type; } - return this.event.type; } + /** * Get the (possibly encrypted) type of the event that will be sent to the * homeserver. * - * @return {string} The event type. + * @returns The event type. */ - - getWireType() { return this.event.type; } + /** - * Get the room_id for this event. This will return undefined - * for m.presence events. - * @return {string?} The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org + * Get the room_id for this event. This will return `undefined` + * for `m.presence` events. + * @returns The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org * */ - - getRoomId() { return this.event.room_id; } + /** * Get the timestamp of this event. - * @return {Number} The event timestamp, e.g. 1433502692297 + * @returns The event timestamp, e.g. `1433502692297` */ - - getTs() { return this.event.origin_server_ts; } + /** * Get the timestamp of this event, as a Date object. - * @return {Date} The event date, e.g. new Date(1433502692297) + * @returns The event date, e.g. `new Date(1433502692297)` */ - - getDate() { return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null; } + + /** + * Get a string containing details of this event + * + * This is intended for logging, to help trace errors. Example output: + * + * @example + * ``` + * id=$HjnOHV646n0SjLDAqFrgIjim7RCpB7cdMXFrekWYAn type=m.room.encrypted + * sender=@user:example.com room=!room:example.com ts=2022-10-25T17:30:28.404Z + * ``` + */ + getDetails() { + let details = `id=${this.getId()} type=${this.getWireType()} sender=${this.getSender()}`; + const room = this.getRoomId(); + if (room) { + details += ` room=${room}`; + } + const date = this.getDate(); + if (date) { + details += ` ts=${date.toISOString()}`; + } + return details; + } + /** * Get the (decrypted, if necessary) event content JSON, even if the event * was replaced by another event. * - * @return {Object} The event content JSON, or an empty object. + * @returns The event content JSON, or an empty object. */ - - getOriginalContent() { if (this._localRedactionEvent) { return {}; } - if (this.clearEvent) { return this.clearEvent.content || {}; } - return this.event.content || {}; } + /** * Get the (decrypted, if necessary) event content JSON, * or the content from the replacing event, if any. * See `makeReplaced`. * - * @return {Object} The event content JSON, or an empty object. + * @returns The event content JSON, or an empty object. */ - - getContent() { if (this._localRedactionEvent) { return {}; @@ -365,146 +365,127 @@ return this.getOriginalContent(); } } + /** * Get the (possibly encrypted) event content JSON that will be sent to the * homeserver. * - * @return {Object} The event content JSON, or an empty object. + * @returns The event content JSON, or an empty object. */ - - getWireContent() { return this.event.content || {}; } + /** - * @experimental * Get the event ID of the thread head */ - - get threadRootId() { const relatesTo = this.getWireContent()?.["m.relates_to"]; - if (relatesTo?.rel_type === _thread.THREAD_RELATION_TYPE.name) { return relatesTo.event_id; } else { return this.getThread()?.id || this.threadId; } } + /** - * @experimental + * A helper to check if an event is a thread's head or not */ - - get isThreadRoot() { - const threadDetails = this.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name); // Bundled relationships only returned when the sync response is limited + const threadDetails = this.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name); + + // Bundled relationships only returned when the sync response is limited // hence us having to check both bundled relation and inspect the thread // model - return !!threadDetails || this.getThread()?.id === this.getId(); } - get replyEventId() { - // We're prefer ev.getContent() over ev.getWireContent() to make sure - // we grab the latest edit with potentially new relations. But we also - // can't just rely on ev.getContent() by itself because historically we - // still show the reply from the original message even though the edit - // event does not include the relation reply. - const mRelatesTo = this.getContent()['m.relates_to'] || this.getWireContent()['m.relates_to']; - return mRelatesTo?.['m.in_reply_to']?.event_id; + return this.getWireContent()["m.relates_to"]?.["m.in_reply_to"]?.event_id; } - get relationEventId() { return this.getWireContent()?.["m.relates_to"]?.event_id; } + /** * Get the previous event content JSON. This will only return something for * state events which exist in the timeline. - * @return {Object} The previous event content JSON, or an empty object. + * @returns The previous event content JSON, or an empty object. */ - - getPrevContent() { // v2 then v1 then default return this.getUnsigned().prev_content || this.event.prev_content || {}; } + /** * Get either 'content' or 'prev_content' depending on if this event is * 'forward-looking' or not. This can be modified via event.forwardLooking. * In practice, this means we get the chronologically earlier content value * for this event (this method should surely be called getEarlierContent) * This method is experimental and may change. - * @return {Object} event.content if this event is forward-looking, else + * @returns event.content if this event is forward-looking, else * event.prev_content. */ - - getDirectionalContent() { return this.forwardLooking ? this.getContent() : this.getPrevContent(); } + /** * Get the age of this event. This represents the age of the event when the * event arrived at the device, and not the age of the event when this * function was called. * Can only be returned once the server has echo'ed back - * @return {Number|undefined} The age of this event in milliseconds. + * @returns The age of this event in milliseconds. */ - - getAge() { return this.getUnsigned().age || this.event.age; // v2 / v1 } + /** * Get the age of the event when this function was called. * This is the 'age' field adjusted according to how long this client has * had the event. - * @return {Number} The age of this event in milliseconds. + * @returns The age of this event in milliseconds. */ - - getLocalAge() { return Date.now() - this.localTimestamp; } + /** * Get the event state_key if it has one. This will return undefined * for message events. - * @return {string} The event's state_key. + * @returns The event's `state_key`. */ - - getStateKey() { return this.event.state_key; } + /** * Check if this event is a state event. - * @return {boolean} True if this is a state event. + * @returns True if this is a state event. */ - - isState() { return this.event.state_key !== undefined; } + /** * Replace the content of this event with encrypted versions. * (This is used when sending an event; it should not be used by applications). * * @internal * - * @param {string} cryptoType type of the encrypted event - typically + * @param cryptoType - type of the encrypted event - typically * "m.room.encrypted" * - * @param {object} cryptoContent raw 'content' for the encrypted event. + * @param cryptoContent - raw 'content' for the encrypted event. * - * @param {string} senderCurve25519Key curve25519 key to record for the + * @param senderCurve25519Key - curve25519 key to record for the * sender of this event. - * See {@link module:models/event.MatrixEvent#getSenderKey}. + * See {@link MatrixEvent#getSenderKey}. * - * @param {string} claimedEd25519Key claimed ed25519 key to record for the + * @param claimedEd25519Key - claimed ed25519 key to record for the * sender if this event. - * See {@link module:models/event.MatrixEvent#getClaimedEd25519Key} + * See {@link MatrixEvent#getClaimedEd25519Key} */ - - makeEncrypted(cryptoType, cryptoContent, senderCurve25519Key, claimedEd25519Key) { // keep the plain-text data for 'view source' this.clearEvent = { @@ -516,34 +497,38 @@ this.senderCurve25519Key = senderCurve25519Key; this.claimedEd25519Key = claimedEd25519Key; } + /** * Check if this event is currently being decrypted. * - * @return {boolean} True if this event is currently being decrypted, else false. + * @returns True if this event is currently being decrypted, else false. */ - - isBeingDecrypted() { - return this._decryptionPromise != null; + return this.decryptionPromise != null; } - getDecryptionPromise() { - return this._decryptionPromise; + return this.decryptionPromise; } + /** * Check if this event is an encrypted event which we failed to decrypt * * (This implies that we might retry decryption at some point in the future) * - * @return {boolean} True if this event is an encrypted event which we + * @returns True if this event is an encrypted event which we * couldn't decrypt. */ - - isDecryptionFailure() { return this.clearEvent?.content?.msgtype === "m.bad.encrypted"; } + /* + * True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and + * the sender has disabled encrypting to unverified devices. + */ + get isEncryptedDisabledForUnverifiedDevices() { + return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices; + } shouldAttemptDecryption() { if (this.isRedacted()) return false; if (this.isBeingDecrypted()) return false; @@ -551,6 +536,7 @@ if (!this.isEncrypted()) return false; return true; } + /** * Start the process of trying to decrypt this event. * @@ -558,61 +544,46 @@ * * @internal * - * @param {module:crypto} crypto crypto module - * @param {object} options - * @param {boolean} options.isRetry True if this is a retry (enables more logging) - * @param {boolean} options.emit Emits "event.decrypted" if set to true + * @param crypto - crypto module * - * @returns {Promise} promise which resolves (to undefined) when the decryption + * @returns promise which resolves (to undefined) when the decryption * attempt is completed. */ - - async attemptDecryption(crypto, options = {}) { - // For backwards compatibility purposes - // The function signature used to be attemptDecryption(crypto, isRetry) - if (typeof options === "boolean") { - options = { - isRetry: options - }; - } // start with a couple of sanity checks. - - + // start with a couple of sanity checks. if (!this.isEncrypted()) { throw new Error("Attempt to decrypt event which isn't encrypted"); } - - if (this.clearEvent && !this.isDecryptionFailure() && !(this.isKeySourceUntrusted() && options.keyTrusted)) { + const alreadyDecrypted = this.clearEvent && !this.isDecryptionFailure(); + const forceRedecrypt = options.forceRedecryptIfUntrusted && this.isKeySourceUntrusted(); + if (alreadyDecrypted && !forceRedecrypt) { // we may want to just ignore this? let's start with rejecting it. throw new Error("Attempt to decrypt event which has already been decrypted"); - } // if we already have a decryption attempt in progress, then it may + } + + // if we already have a decryption attempt in progress, then it may // fail because it was using outdated info. We now have reason to // succeed where it failed before, but we don't want to have multiple // attempts going at the same time, so just set a flag that says we have // new info. // - - - if (this._decryptionPromise) { + if (this.decryptionPromise) { _logger.logger.log(`Event ${this.getId()} already being decrypted; queueing a retry`); - this.retryDecryption = true; - return this._decryptionPromise; + return this.decryptionPromise; } - - this._decryptionPromise = this.decryptionLoop(crypto, options); - return this._decryptionPromise; + this.decryptionPromise = this.decryptionLoop(crypto, options); + return this.decryptionPromise; } + /** * Cancel any room key request for this event and resend another. * - * @param {module:crypto} crypto crypto module - * @param {string} userId the user who received this event + * @param crypto - crypto module + * @param userId - the user who received this event * - * @returns {Promise} a promise that resolves when the request is queued + * @returns a promise that resolves when the request is queued */ - - cancelAndResendKeyRequest(crypto, userId) { const wireContent = this.getWireContent(); return crypto.requestRoomKey({ @@ -622,129 +593,103 @@ sender_key: wireContent.sender_key }, this.getKeyRequestRecipients(userId), true); } + /** * Calculate the recipients for keyshare requests. * - * @param {string} userId the user who received this event. + * @param userId - the user who received this event. * - * @returns {Array} array of recipients + * @returns array of recipients */ - - getKeyRequestRecipients(userId) { - // send the request to all of our own devices, and the - // original sending device if it wasn't us. - const wireContent = this.getWireContent(); + // send the request to all of our own devices const recipients = [{ userId, - deviceId: '*' + deviceId: "*" }]; - const sender = this.getSender(); - - if (sender !== userId) { - recipients.push({ - userId: sender, - deviceId: wireContent.device_id - }); - } - return recipients; } - async decryptionLoop(crypto, options = {}) { // make sure that this method never runs completely synchronously. - // (doing so would mean that we would clear _decryptionPromise *before* + // (doing so would mean that we would clear decryptionPromise *before* // it is set in attemptDecryption - and hence end up with a stuck - // `_decryptionPromise`). - await Promise.resolve(); // eslint-disable-next-line no-constant-condition + // `decryptionPromise`). + await Promise.resolve(); + // eslint-disable-next-line no-constant-condition while (true) { this.retryDecryption = false; let res; - let err; - + let err = undefined; try { if (!crypto) { res = this.badEncryptedMessage("Encryption not enabled"); } else { res = await crypto.decryptEvent(this); - if (options.isRetry === true) { - _logger.logger.info(`Decrypted event on retry (id=${this.getId()})`); + _logger.logger.info(`Decrypted event on retry (${this.getDetails()})`); } } } catch (e) { - if (e.name !== "DecryptionError") { - // not a decryption error: log the whole exception as an error - // (and don't bother with a retry) - const re = options.isRetry ? 're' : ''; // For find results: this can produce "Error decrypting event (id=$ev)" and - // "Error redecrypting event (id=$ev)". - - _logger.logger.error(`Error ${re}decrypting event ` + `(id=${this.getId()}): ${e.stack || e}`); - - this._decryptionPromise = null; - this.retryDecryption = false; - return; - } + const detailedError = e instanceof _algorithms.DecryptionError ? e.detailedString : String(e); + err = e; - err = e; // see if we have a retry queued. + // see if we have a retry queued. // // NB: make sure to keep this check in the same tick of the - // event loop as `_decryptionPromise = null` below - otherwise we + // event loop as `decryptionPromise = null` below - otherwise we // risk a race: // // * A: we check retryDecryption here and see that it is // false // * B: we get a second call to attemptDecryption, which sees - // that _decryptionPromise is set so sets + // that decryptionPromise is set so sets // retryDecryption - // * A: we continue below, clear _decryptionPromise, and + // * A: we continue below, clear decryptionPromise, and // never do the retry. // - if (this.retryDecryption) { // decryption error, but we have a retry queued. - _logger.logger.log(`Got error decrypting event (id=${this.getId()}: ${e.detailedString}), but retrying`, e); - + _logger.logger.log(`Error decrypting event (${this.getDetails()}), but retrying: ${detailedError}`); continue; - } // decryption error, no retries queued. Warn about the error and - // set it to m.bad.encrypted. - + } - _logger.logger.warn(`Got error decrypting event (id=${this.getId()}: ${e.detailedString})`, e); + // decryption error, no retries queued. Warn about the error and + // set it to m.bad.encrypted. + // + // the detailedString already includes the name and message of the error, and the stack isn't much use, + // so we don't bother to log `e` separately. + _logger.logger.warn(`Error decrypting event (${this.getDetails()}): ${detailedError}`); + res = this.badEncryptedMessage(String(e)); + } - res = this.badEncryptedMessage(e.message); - } // at this point, we've either successfully decrypted the event, or have given up + // at this point, we've either successfully decrypted the event, or have given up // (and set res to a 'badEncryptedMessage'). Either way, we can now set the // cleartext of the event and raise Event.decrypted. // - // make sure we clear '_decryptionPromise' before sending the 'Event.decrypted' event, + // make sure we clear 'decryptionPromise' before sending the 'Event.decrypted' event, // otherwise the app will be confused to see `isBeingDecrypted` still set when // there isn't an `Event.decrypted` on the way. // // see also notes on retryDecryption above. // - - - this._decryptionPromise = null; + this.decryptionPromise = null; this.retryDecryption = false; - this.setClearData(res); // Before we emit the event, clear the push actions so that they can be recalculated + this.setClearData(res); + + // Before we emit the event, clear the push actions so that they can be recalculated // by relevant code. We do this because the clear event has now changed, making it // so that existing rules can be re-run over the applicable properties. Stuff like // highlighting when the user's name is mentioned rely on this happening. We also want // to set the push actions before emitting so that any notification listeners don't // pick up the wrong contents. - this.setPushActions(null); - if (options.emit !== false) { this.emit(MatrixEventEvent.Decrypted, this, err); } - return; } } - badEncryptedMessage(reason) { return { clearEvent: { @@ -753,9 +698,11 @@ msgtype: "m.bad.encrypted", body: "** Unable to decrypt: " + reason + " **" } - } + }, + encryptedDisabledForUnverifiedDevices: reason === `DecryptionError: ${_OlmDevice.WITHHELD_MESSAGES["m.unverified"]}` }; } + /** * Update the cleartext data on this event. * @@ -763,41 +710,39 @@ * * @internal * - * @fires module:models/event.MatrixEvent#"Event.decrypted" + * @param decryptionResult - the decryption result, including the plaintext and some key info * - * @param {module:crypto~EventDecryptionResult} decryptionResult - * the decryption result, including the plaintext and some key info + * @remarks + * Fires {@link MatrixEventEvent.Decrypted} */ - - setClearData(decryptionResult) { this.clearEvent = decryptionResult.clearEvent; - this.senderCurve25519Key = decryptionResult.senderCurve25519Key || null; - this.claimedEd25519Key = decryptionResult.claimedEd25519Key || null; + this.senderCurve25519Key = decryptionResult.senderCurve25519Key ?? null; + this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null; this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || []; this.untrusted = decryptionResult.untrusted || false; + this.encryptedDisabledForUnverifiedDevices = decryptionResult.encryptedDisabledForUnverifiedDevices || false; this.invalidateExtensibleEvent(); } + /** * Gets the cleartext content for this event. If the event is not encrypted, * or encryption has not been completed, this will return null. * - * @returns {Object} The cleartext (decrypted) content for the event + * @returns The cleartext (decrypted) content for the event */ - - getClearContent() { return this.clearEvent ? this.clearEvent.content : null; } + /** * Check if the event is encrypted. - * @return {boolean} True if this event is encrypted. + * @returns True if this event is encrypted. */ - - isEncrypted() { return !this.isState() && this.event.type === _event.EventType.RoomMessageEncrypted; } + /** * The curve25519 key for the device that we think sent this event * @@ -808,28 +753,23 @@ * * For a megolm-encrypted event, it is inferred from the Olm message which * established the megolm session - * - * @return {string} */ - - getSenderKey() { return this.senderCurve25519Key; } + /** * The additional keys the sender of this encrypted event claims to possess. * * Just a wrapper for #getClaimedEd25519Key (q.v.) - * - * @return {Object} */ - - getKeysClaimed() { + if (!this.claimedEd25519Key) return {}; return { ed25519: this.claimedEd25519Key }; } + /** * Get the ed25519 the sender of this event claims to own. * @@ -844,14 +784,11 @@ * * In general, applications should not use this method directly, but should * instead use MatrixClient.getEventSenderDeviceInfo. - * - * @return {string} */ - - getClaimedEd25519Key() { return this.claimedEd25519Key; } + /** * Get the curve25519 keys of the devices which were involved in telling us * about the claimedEd25519Key and sender curve25519 key. @@ -864,177 +801,152 @@ * If the device that sent us the key (A) got it from another device which * it wasn't prepared to vouch for (B), the result will be [A, B]. And so on. * - * @return {string[]} base64-encoded curve25519 keys, from oldest to newest. + * @returns base64-encoded curve25519 keys, from oldest to newest. */ - - getForwardingCurve25519KeyChain() { return this.forwardingCurve25519KeyChain; } + /** * Whether the decryption key was obtained from an untrusted source. If so, * we cannot verify the authenticity of the message. - * - * @return {boolean} */ - - isKeySourceUntrusted() { - return this.untrusted; + return !!this.untrusted; } - getUnsigned() { return this.event.unsigned || {}; } - setUnsigned(unsigned) { this.event.unsigned = unsigned; } - unmarkLocallyRedacted() { const value = this._localRedactionEvent; this._localRedactionEvent = null; - if (this.event.unsigned) { - this.event.unsigned.redacted_because = null; + this.event.unsigned.redacted_because = undefined; } - return !!value; } - markLocallyRedacted(redactionEvent) { if (this._localRedactionEvent) return; this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent); this._localRedactionEvent = redactionEvent; - if (!this.event.unsigned) { this.event.unsigned = {}; } - this.event.unsigned.redacted_because = redactionEvent.event; } + /** * Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 . * - * @fires module:models/event.MatrixEvent#"Event.visibilityChange" if `visibilityEvent` + * @param visibilityChange - event holding a hide/unhide payload, or nothing + * if the event is being reset to its original visibility (presumably + * by a visibility event being redacted). + * + * @remarks + * Fires {@link MatrixEventEvent.VisibilityChange} if `visibilityEvent` * caused a change in the actual visibility of this event, either by making it * visible (if it was hidden), by making it hidden (if it was visible) or by * changing the reason (if it was hidden). - * @param visibilityChange event holding a hide/unhide payload, or nothing - * if the event is being reset to its original visibility (presumably - * by a visibility event being redacted). */ - - applyVisibilityEvent(visibilityChange) { - const visible = visibilityChange ? visibilityChange.visible : true; - const reason = visibilityChange ? visibilityChange.reason : null; + const visible = visibilityChange?.visible ?? true; + const reason = visibilityChange?.reason ?? null; let change = false; - - if (this.visibility.visible !== visibilityChange.visible) { + if (this.visibility.visible !== visible) { change = true; } else if (!this.visibility.visible && this.visibility["reason"] !== reason) { change = true; } - if (change) { if (visible) { this.visibility = MESSAGE_VISIBLE; } else { this.visibility = Object.freeze({ visible: false, - reason: reason + reason }); } - this.emit(MatrixEventEvent.VisibilityChange, this, visible); } } + /** * Return instructions to display or hide the message. * * @returns Instructions determining whether the message * should be displayed. */ - - messageVisibility() { // Note: We may return `this.visibility` without fear, as // this is a shallow frozen object. return this.visibility; } + /** * Update the content of an event in the same way it would be by the server * if it were redacted before it was sent to us * - * @param {module:models/event.MatrixEvent} redactionEvent - * event causing the redaction + * @param redactionEvent - event causing the redaction */ - - makeRedacted(redactionEvent) { // quick sanity-check if (!redactionEvent.event) { throw new Error("invalid redactionEvent in makeRedacted"); } - this._localRedactionEvent = null; this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent); - this._replacingEvent = null; // we attempt to replicate what we would see from the server if + this._replacingEvent = null; + // we attempt to replicate what we would see from the server if // the event had been redacted before we saw it. // // The server removes (most of) the content of the event, and adds a // "redacted_because" key to the unsigned section containing the // redacted event. - if (!this.event.unsigned) { this.event.unsigned = {}; } - this.event.unsigned.redacted_because = redactionEvent.event; - for (const key in this.event) { if (this.event.hasOwnProperty(key) && !REDACT_KEEP_KEYS.has(key)) { delete this.event[key]; } - } // If the event is encrypted prune the decrypted bits - + } + // If the event is encrypted prune the decrypted bits if (this.isEncrypted()) { - this.clearEvent = null; + this.clearEvent = undefined; } - - const keeps = REDACT_KEEP_CONTENT_MAP[this.getType()] || {}; + const keeps = this.getType() in REDACT_KEEP_CONTENT_MAP ? REDACT_KEEP_CONTENT_MAP[this.getType()] : {}; const content = this.getContent(); - for (const key in content) { if (content.hasOwnProperty(key) && !keeps[key]) { delete content[key]; } } - this.invalidateExtensibleEvent(); } + /** * Check if this event has been redacted * - * @return {boolean} True if this event has been redacted + * @returns True if this event has been redacted */ - - isRedacted() { return Boolean(this.getUnsigned().redacted_because); } + /** * Check if this event is a redaction of another event * - * @return {boolean} True if this event is a redaction + * @returns True if this event is a redaction */ - - isRedaction() { return this.getType() === _event.EventType.RoomRedaction; } + /** * Return the visibility change caused by this event, * as per https://github.com/matrix-org/matrix-doc/pull/3531. @@ -1042,200 +954,168 @@ * @returns If the event is a well-formed visibility change event, * an instance of `IVisibilityChange`, otherwise `null`. */ - - asVisibilityChange() { if (!_event.EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) { // Not a visibility change event. return null; } - const relation = this.getRelation(); - if (!relation || relation.rel_type != "m.reference") { // Ill-formed, ignore this event. return null; } - const eventId = relation.event_id; - if (!eventId) { // Ill-formed, ignore this event. return null; } - const content = this.getWireContent(); const visible = !!content.visible; const reason = content.reason; - if (reason && typeof reason != "string") { // Ill-formed, ignore this event. return null; - } // Well-formed visibility change event. - - + } + // Well-formed visibility change event. return { visible, reason, eventId }; } + /** * Check if this event alters the visibility of another event, * as per https://github.com/matrix-org/matrix-doc/pull/3531. * - * @returns {boolean} True if this event alters the visibility + * @returns True if this event alters the visibility * of another event. */ - - isVisibilityEvent() { return _event.EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType()); } + /** * Get the (decrypted, if necessary) redaction event JSON * if event was redacted * - * @returns {object} The redaction event JSON, or an empty object + * @returns The redaction event JSON, or an empty object */ - - getRedactionEvent() { if (!this.isRedacted()) return null; - if (this.clearEvent?.unsigned) { - return this.clearEvent?.unsigned.redacted_because; - } else if (this.event.unsigned.redacted_because) { + return this.clearEvent?.unsigned.redacted_because ?? null; + } else if (this.event.unsigned?.redacted_because) { return this.event.unsigned.redacted_because; } else { return {}; } } + /** * Get the push actions, if known, for this event * - * @return {?Object} push actions + * @returns push actions */ - - getPushActions() { return this.pushActions; } + /** * Set the push actions for this event. * - * @param {Object} pushActions push actions + * @param pushActions - push actions */ - - setPushActions(pushActions) { this.pushActions = pushActions; } + /** * Replace the `event` property and recalculate any properties based on it. - * @param {Object} event the object to assign to the `event` property + * @param event - the object to assign to the `event` property */ - - handleRemoteEcho(event) { const oldUnsigned = this.getUnsigned(); const oldId = this.getId(); - this.event = event; // if this event was redacted before it was sent, it's locally marked as redacted. + this.event = event; + // if this event was redacted before it was sent, it's locally marked as redacted. // At this point, we've received the remote echo for the event, but not yet for // the redaction that we are sending ourselves. Preserve the locally redacted // state by copying over redacted_because so we don't get a flash of // redacted, not-redacted, redacted as remote echos come in - if (oldUnsigned.redacted_because) { if (!this.event.unsigned) { this.event.unsigned = {}; } - this.event.unsigned.redacted_because = oldUnsigned.redacted_because; - } // successfully sent. - - + } + // successfully sent. this.setStatus(null); - if (this.getId() !== oldId) { // emit the event if it changed this.emit(MatrixEventEvent.LocalEventIdReplaced, this); } - this.localTimestamp = Date.now() - this.getAge(); } + /** * Whether the event is in any phase of sending, send failure, waiting for * remote echo, etc. - * - * @return {boolean} */ - - isSending() { return !!this.status; } + /** * Update the event's sending status and emit an event as well. * - * @param {String} status The new status + * @param status - The new status */ - - setStatus(status) { this.status = status; this.emit(MatrixEventEvent.Status, this, status); } - replaceLocalEventId(eventId) { this.event.event_id = eventId; this.emit(MatrixEventEvent.LocalEventIdReplaced, this); } + /** * Get whether the event is a relation event, and of a given type if * `relType` is passed in. State events cannot be relation events * - * @param {string?} relType if given, checks that the relation is of the + * @param relType - if given, checks that the relation is of the * given type - * @return {boolean} */ - - - isRelation(relType = undefined) { + isRelation(relType) { // Relation info is lifted out of the encrypted content when sent to // encrypted rooms, so we have to check `getWireContent` for this. const relation = this.getWireContent()?.["m.relates_to"]; - if (this.isState() && relation?.rel_type === _event.RelationType.Replace) { // State events cannot be m.replace relations return false; } - - return relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true); + return !!(relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true)); } + /** * Get relation info for the event, if any. - * - * @return {Object} */ - - getRelation() { if (!this.isRelation()) { return null; } - - return this.getWireContent()["m.relates_to"]; + return this.getWireContent()["m.relates_to"] ?? null; } + /** * Set an event that replaces the content of this event, through an m.replace relation. * - * @fires module:models/event.MatrixEvent#"Event.replaced" + * @param newEvent - the event with the replacing content, if any. * - * @param {MatrixEvent?} newEvent the event with the replacing content, if any. + * @remarks + * Fires {@link MatrixEventEvent.Replaced} */ - - makeReplaced(newEvent) { // don't allow redacted events to be replaced. // if newEvent is null we allow to go through though, @@ -1243,108 +1123,84 @@ // cancelled, which should be reflected on the target event. if (this.isRedacted() && newEvent) { return; - } // don't allow state events to be replaced using this mechanism as per MSC2676 - - + } + // don't allow state events to be replaced using this mechanism as per MSC2676 if (this.isState()) { return; } - if (this._replacingEvent !== newEvent) { - this._replacingEvent = newEvent; + this._replacingEvent = newEvent ?? null; this.emit(MatrixEventEvent.Replaced, this); this.invalidateExtensibleEvent(); } } + /** * Returns the status of any associated edit or redaction * (not for reactions/annotations as their local echo doesn't affect the original event), * or else the status of the event. - * - * @return {EventStatus} */ - - getAssociatedStatus() { if (this._replacingEvent) { return this._replacingEvent.status; } else if (this._localRedactionEvent) { return this._localRedactionEvent.status; } - return this.status; } - getServerAggregatedRelation(relType) { return this.getUnsigned()["m.relations"]?.[relType]; } + /** * Returns the event ID of the event replacing the content of this event, if any. - * - * @return {string?} */ - - replacingEventId() { const replaceRelation = this.getServerAggregatedRelation(_event.RelationType.Replace); - if (replaceRelation) { return replaceRelation.event_id; } else if (this._replacingEvent) { return this._replacingEvent.getId(); } } + /** * Returns the event replacing the content of this event, if any. * Replacements are aggregated on the server, so this would only * return an event in case it came down the sync, or for local echo of edits. - * - * @return {MatrixEvent?} */ - - replacingEvent() { return this._replacingEvent; } + /** * Returns the origin_server_ts of the event replacing the content of this event, if any. - * - * @return {Date?} */ - - replacingEventDate() { const replaceRelation = this.getServerAggregatedRelation(_event.RelationType.Replace); - if (replaceRelation) { const ts = replaceRelation.origin_server_ts; - if (Number.isFinite(ts)) { return new Date(ts); } } else if (this._replacingEvent) { - return this._replacingEvent.getDate(); + return this._replacingEvent.getDate() ?? undefined; } } + /** * Returns the event that wants to redact this event, but hasn't been sent yet. - * @return {MatrixEvent} the event + * @returns the event */ - - localRedactionEvent() { return this._localRedactionEvent; } + /** * For relations and redactions, returns the event_id this event is referring to. - * - * @return {string?} */ - - getAssociatedId() { const relation = this.getRelation(); - if (this.replyEventId) { return this.replyEventId; } else if (relation) { @@ -1353,56 +1209,58 @@ return this.event.redacts; } } + /** * Checks if this event is associated with another event. See `getAssociatedId`. - * - * @return {boolean} + * @deprecated use hasAssociation instead. */ - - hasAssocation() { return !!this.getAssociatedId(); } + + /** + * Checks if this event is associated with another event. See `getAssociatedId`. + */ + hasAssociation() { + return !!this.getAssociatedId(); + } + /** * Update the related id with a new one. * * Used to replace a local id with remote one before sending * an event with a related id. * - * @param {string} eventId the new event id + * @param eventId - the new event id */ - - updateAssociatedId(eventId) { const relation = this.getRelation(); - if (relation) { relation.event_id = eventId; } else if (this.isRedaction()) { this.event.redacts = eventId; } } + /** * Flags an event as cancelled due to future conditions. For example, a verification * request event in the same sync transaction may be flagged as cancelled to warn * listeners that a cancellation event is coming down the same pipe shortly. - * @param {boolean} cancelled Whether the event is to be cancelled or not. + * @param cancelled - Whether the event is to be cancelled or not. */ - - flagCancelled(cancelled = true) { this._isCancelled = cancelled; } + /** * Gets whether or not the event is flagged as cancelled. See flagCancelled() for * more information. - * @returns {boolean} True if the event is cancelled, false otherwise. + * @returns True if the event is cancelled, false otherwise. */ - - isCancelled() { return this._isCancelled; } + /** * Get a copy/snapshot of this event. The returned copy will be loosely linked * back to this instance, though will have "frozen" event information. Other @@ -1415,31 +1273,27 @@ * * This is meant to be used to snapshot the event details themselves, not the * features (such as sender) surrounding the event. - * @returns {MatrixEvent} A snapshot of this event. + * @returns A snapshot of this event. */ - - toSnapshot() { const ev = new MatrixEvent(JSON.parse(JSON.stringify(this.event))); - for (const [p, v] of Object.entries(this)) { if (p !== "event") { // exclude the thing we just cloned + // @ts-ignore - XXX: this is just nasty ev[p] = v; } } - return ev; } + /** * Determines if this event is equivalent to the given event. This only checks * the event object itself, not the other properties of the event. Intended for * use with toSnapshot() to identify events changing. - * @param {MatrixEvent} otherEvent The other event to check against. - * @returns {boolean} True if the events are the same, false otherwise. + * @param otherEvent - The other event to check against. + * @returns True if the events are the same, false otherwise. */ - - isEquivalentTo(otherEvent) { if (!otherEvent) return false; if (otherEvent === this) return true; @@ -1447,6 +1301,7 @@ const theirProps = (0, _utils.deepSortedObjectEntries)(otherEvent.event); return JSON.stringify(myProps) === JSON.stringify(theirProps); } + /** * Summarise the event as JSON. This is currently used by React SDK's view * event source feature and Seshat's event indexing, so take care when @@ -1457,59 +1312,53 @@ * This is named `toJSON` for use with `JSON.stringify` which checks objects * for functions named `toJSON` and will call them to customise the output * if they are defined. - * - * @return {Object} */ - - toJSON() { const event = this.getEffectiveEvent(); - if (!this.isEncrypted()) { return event; } - return { decrypted: event, encrypted: this.event }; } - setVerificationRequest(request) { this.verificationRequest = request; } - setTxnId(txnId) { this.txnId = txnId; } - getTxnId() { return this.txnId; } + /** - * @experimental + * Set the instance of a thread associated with the current event + * @param thread - the thread */ - - setThread(thread) { + if (this.thread) { + this.reEmitter.stopReEmitting(this.thread, [_thread.ThreadEvent.Update]); + } this.thread = thread; - this.setThreadId(thread.id); - this.reEmitter.reEmit(thread, [_thread.ThreadEvent.Update]); + this.setThreadId(thread?.id); + if (thread) { + this.reEmitter.reEmit(thread, [_thread.ThreadEvent.Update]); + } } + /** - * @experimental + * Get the instance of the thread associated with the current event */ - - getThread() { return this.thread; } - setThreadId(threadId) { this.threadId = threadId; } - } + /* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted * * This is specified here: @@ -1519,43 +1368,28 @@ * - We keep 'unsigned' since that is created by the local server * - We keep user_id for backwards-compat with v1 */ - - exports.MatrixEvent = MatrixEvent; -const REDACT_KEEP_KEYS = new Set(['event_id', 'type', 'room_id', 'user_id', 'sender', 'state_key', 'prev_state', 'content', 'unsigned', 'origin_server_ts']); // a map from state event type to the .content keys we keep when an event is redacted +const REDACT_KEEP_KEYS = new Set(["event_id", "type", "room_id", "user_id", "sender", "state_key", "prev_state", "content", "unsigned", "origin_server_ts"]); +// a map from state event type to the .content keys we keep when an event is redacted const REDACT_KEEP_CONTENT_MAP = { [_event.EventType.RoomMember]: { - 'membership': 1 + membership: 1 }, [_event.EventType.RoomCreate]: { - 'creator': 1 + creator: 1 }, [_event.EventType.RoomJoinRules]: { - 'join_rule': 1 + join_rule: 1 }, [_event.EventType.RoomPowerLevels]: { - 'ban': 1, - 'events': 1, - 'events_default': 1, - 'kick': 1, - 'redact': 1, - 'state_default': 1, - 'users': 1, - 'users_default': 1 - }, - [_event.EventType.RoomAliases]: { - 'aliases': 1 + ban: 1, + events: 1, + events_default: 1, + kick: 1, + redact: 1, + state_default: 1, + users: 1, + users_default: 1 } -}; -/** - * Fires when an event is decrypted - * - * @event module:models/event.MatrixEvent#"Event.decrypted" - * - * @param {module:models/event.MatrixEvent} event - * The matrix event which has been decrypted - * @param {module:crypto/algorithms/base.DecryptionError?} err - * The error that occurred during decryption, or `undefined` if no - * error occurred. - */ \ No newline at end of file +}; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-status.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-status.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-status.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-status.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,7 +4,6 @@ value: true }); exports.EventStatus = void 0; - /* Copyright 2015 - 2022 The Matrix.org Foundation C.I.C. @@ -20,15 +19,12 @@ See the License for the specific language governing permissions and limitations under the License. */ - /** * Enum for event statuses. * @readonly - * @enum {string} */ let EventStatus; exports.EventStatus = EventStatus; - (function (EventStatus) { EventStatus["NOT_SENT"] = "not_sent"; EventStatus["ENCRYPTING"] = "encrypting"; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,23 +4,18 @@ value: true }); exports.EventTimeline = exports.Direction = void 0; - var _logger = require("../logger"); - var _roomState = require("./room-state"); - var _event = require("../@types/event"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } let Direction; exports.Direction = Direction; - (function (Direction) { Direction["Backward"] = "b"; Direction["Forward"] = "f"; })(Direction || (exports.Direction = Direction = {})); - class EventTimeline { /** * Symbolic constant for methods which take a 'direction' argument: @@ -35,9 +30,9 @@ /** * Static helper method to set sender and target properties * - * @param {MatrixEvent} event the event whose metadata is to be set - * @param {RoomState} stateContext the room state to be queried - * @param {boolean} toStartOfTimeline if true the event's forwardLooking flag is set false + * @param event - the event whose metadata is to be set + * @param stateContext - the room state to be queried + * @param toStartOfTimeline - if true the event's forwardLooking flag is set false */ static setEventMetadata(event, stateContext, toStartOfTimeline) { // When we try to generate a sentinel member before we have that member @@ -48,11 +43,9 @@ if (!event.sender?.events?.member) { event.sender = stateContext.getSentinelMember(event.getSender()); } - if (!event.target?.events?.member && event.getType() === _event.EventType.RoomMember) { event.target = stateContext.getSentinelMember(event.getStateKey()); } - if (event.isState()) { // room state has no concept of 'old' or 'current', but we want the // room state to regress back to previous values if toStartOfTimeline @@ -63,7 +56,6 @@ } } } - /** * Construct a new EventTimeline * @@ -81,160 +73,128 @@ *

Once a timeline joins up with its neighbour, they are linked together into a * doubly-linked list. * - * @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of - * @constructor + * @param eventTimelineSet - the set of timelines this is part of */ constructor(eventTimelineSet) { this.eventTimelineSet = eventTimelineSet; - _defineProperty(this, "roomId", void 0); - _defineProperty(this, "name", void 0); - _defineProperty(this, "events", []); - _defineProperty(this, "baseIndex", 0); - _defineProperty(this, "startState", void 0); - _defineProperty(this, "endState", void 0); - - _defineProperty(this, "prevTimeline", void 0); - - _defineProperty(this, "nextTimeline", void 0); - + _defineProperty(this, "startToken", null); + _defineProperty(this, "endToken", null); + _defineProperty(this, "prevTimeline", null); + _defineProperty(this, "nextTimeline", null); _defineProperty(this, "paginationRequests", { [Direction.Backward]: null, [Direction.Forward]: null }); - this.roomId = eventTimelineSet.room?.roomId ?? null; - this.startState = new _roomState.RoomState(this.roomId); - this.startState.paginationToken = null; - this.endState = new _roomState.RoomState(this.roomId); - this.endState.paginationToken = null; - this.prevTimeline = null; - this.nextTimeline = null; // this is used by client.js + if (this.roomId) { + this.startState = new _roomState.RoomState(this.roomId); + this.endState = new _roomState.RoomState(this.roomId); + } + // this is used by client.js this.paginationRequests = { - 'b': null, - 'f': null + b: null, + f: null }; this.name = this.roomId + ":" + new Date().toISOString(); } + /** * Initialise the start and end state with the given events * *

This can only be called before any events are added. * - * @param {MatrixEvent[]} stateEvents list of state events to initialise the + * @param stateEvents - list of state events to initialise the * state with. - * @throws {Error} if an attempt is made to call this after addEvent is called. + * @throws Error if an attempt is made to call this after addEvent is called. */ - - initialiseState(stateEvents, { timelineWasEmpty } = {}) { if (this.events.length > 0) { throw new Error("Cannot initialise state after events are added"); - } // We previously deep copied events here and used different copies in - // the oldState and state events: this decision seems to date back - // quite a way and was apparently made to fix a bug where modifications - // made to the start state leaked through to the end state. - // This really shouldn't be possible though: the events themselves should - // not change. Duplicating the events uses a lot of extra memory, - // so we now no longer do it. To assert that they really do never change, - // freeze them! Note that we can't do this for events in general: - // although it looks like the only things preventing us are the - // 'status' flag, forwardLooking (which is only set once when adding to the - // timeline) and possibly the sender (which seems like it should never be - // reset but in practice causes a lot of the tests to break). - - - for (const e of stateEvents) { - Object.freeze(e); } - - this.startState.setStateEvents(stateEvents, { + this.startState?.setStateEvents(stateEvents, { timelineWasEmpty }); - this.endState.setStateEvents(stateEvents, { + this.endState?.setStateEvents(stateEvents, { timelineWasEmpty }); } + /** * Forks the (live) timeline, taking ownership of the existing directional state of this timeline. * All attached listeners will keep receiving state updates from the new live timeline state. * The end state of this timeline gets replaced with an independent copy of the current RoomState, * and will need a new pagination token if it ever needs to paginate forwards. - * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * @param direction - EventTimeline.BACKWARDS to get the state at the * start of the timeline; EventTimeline.FORWARDS to get the state at the end * of the timeline. * - * @return {EventTimeline} the new timeline + * @returns the new timeline */ - - forkLive(direction) { const forkState = this.getState(direction); const timeline = new EventTimeline(this.eventTimelineSet); - timeline.startState = forkState.clone(); // Now clobber the end state of the new live timeline with that from the + timeline.startState = forkState?.clone(); + // Now clobber the end state of the new live timeline with that from the // previous live timeline. It will be identical except that we'll keep // using the same RoomMember objects for the 'live' set of members with any // listeners still attached - - timeline.endState = forkState; // Firstly, we just stole the current timeline's end state, so it needs a new one. + timeline.endState = forkState; + // Firstly, we just stole the current timeline's end state, so it needs a new one. // Make an immutable copy of the state so back pagination will get the correct sentinels. - - this.endState = forkState.clone(); + this.endState = forkState?.clone(); return timeline; } + /** * Creates an independent timeline, inheriting the directional state from this timeline. * - * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * @param direction - EventTimeline.BACKWARDS to get the state at the * start of the timeline; EventTimeline.FORWARDS to get the state at the end * of the timeline. * - * @return {EventTimeline} the new timeline + * @returns the new timeline */ - - fork(direction) { const forkState = this.getState(direction); const timeline = new EventTimeline(this.eventTimelineSet); - timeline.startState = forkState.clone(); - timeline.endState = forkState.clone(); + timeline.startState = forkState?.clone(); + timeline.endState = forkState?.clone(); return timeline; } + /** * Get the ID of the room for this timeline - * @return {string} room ID + * @returns room ID */ - - getRoomId() { return this.roomId; } + /** * Get the filter for this timeline's timelineSet (if any) - * @return {Filter} filter + * @returns filter */ - - getFilter() { return this.eventTimelineSet.getFilter(); } + /** * Get the timelineSet for this timeline - * @return {EventTimelineSet} timelineSet + * @returns timelineSet */ - - getTimelineSet() { return this.eventTimelineSet; } + /** * Get the base index. * @@ -243,35 +203,29 @@ * relative to the base index (although note that a given event's index may * well be less than the base index, thus giving that event a negative relative * index). - * - * @return {number} */ - - getBaseIndex() { return this.baseIndex; } + /** * Get the list of events in this context * - * @return {MatrixEvent[]} An array of MatrixEvents + * @returns An array of MatrixEvents */ - - getEvents() { return this.events; } + /** * Get the room state at the start/end of the timeline * - * @param {string} direction EventTimeline.BACKWARDS to get the state at the + * @param direction - EventTimeline.BACKWARDS to get the state at the * start of the timeline; EventTimeline.FORWARDS to get the state at the end * of the timeline. * - * @return {RoomState} state at the start/end of the timeline + * @returns state at the start/end of the timeline */ - - getState(direction) { if (direction == EventTimeline.BACKWARDS) { return this.startState; @@ -281,45 +235,54 @@ throw new Error("Invalid direction '" + direction + "'"); } } + /** * Get a pagination token * - * @param {string} direction EventTimeline.BACKWARDS to get the pagination + * @param direction - EventTimeline.BACKWARDS to get the pagination * token for going backwards in time; EventTimeline.FORWARDS to get the * pagination token for going forwards in time. * - * @return {?string} pagination token + * @returns pagination token */ - - getPaginationToken(direction) { - return this.getState(direction).paginationToken; + if (this.roomId) { + return this.getState(direction).paginationToken; + } else if (direction === Direction.Backward) { + return this.startToken; + } else { + return this.endToken; + } } + /** * Set a pagination token * - * @param {?string} token pagination token + * @param token - pagination token * - * @param {string} direction EventTimeline.BACKWARDS to set the paginatio + * @param direction - EventTimeline.BACKWARDS to set the pagination * token for going backwards in time; EventTimeline.FORWARDS to set the * pagination token for going forwards in time. */ - - setPaginationToken(token, direction) { - this.getState(direction).paginationToken = token; + if (this.roomId) { + this.getState(direction).paginationToken = token; + } else if (direction === Direction.Backward) { + this.startToken = token; + } else { + this.endToken = token; + } } + /** * Get the next timeline in the series * - * @param {string} direction EventTimeline.BACKWARDS to get the previous + * @param direction - EventTimeline.BACKWARDS to get the previous * timeline; EventTimeline.FORWARDS to get the next timeline. * - * @return {?EventTimeline} previous or following timeline, if they have been + * @returns previous or following timeline, if they have been * joined up. */ - - getNeighbouringTimeline(direction) { if (direction == EventTimeline.BACKWARDS) { return this.prevTimeline; @@ -329,48 +292,45 @@ throw new Error("Invalid direction '" + direction + "'"); } } + /** * Set the next timeline in the series * - * @param {EventTimeline} neighbour previous/following timeline + * @param neighbour - previous/following timeline * - * @param {string} direction EventTimeline.BACKWARDS to set the previous + * @param direction - EventTimeline.BACKWARDS to set the previous * timeline; EventTimeline.FORWARDS to set the next timeline. * - * @throws {Error} if an attempt is made to set the neighbouring timeline when + * @throws Error if an attempt is made to set the neighbouring timeline when * it is already set. */ - - setNeighbouringTimeline(neighbour, direction) { if (this.getNeighbouringTimeline(direction)) { throw new Error("timeline already has a neighbouring timeline - " + "cannot reset neighbour (direction: " + direction + ")"); } - if (direction == EventTimeline.BACKWARDS) { this.prevTimeline = neighbour; } else if (direction == EventTimeline.FORWARDS) { this.nextTimeline = neighbour; } else { throw new Error("Invalid direction '" + direction + "'"); - } // make sure we don't try to paginate this timeline - + } + // make sure we don't try to paginate this timeline this.setPaginationToken(null, direction); } + /** * Add a new event to the timeline, and update the state * - * @param {MatrixEvent} event new event - * @param {IAddEventOptions} options addEvent options + * @param event - new event + * @param options - addEvent options */ - addEvent(event, toStartOfTimelineOrOpts, roomState) { let toStartOfTimeline = !!toStartOfTimelineOrOpts; let timelineWasEmpty; - - if (typeof toStartOfTimelineOrOpts === 'object') { + if (typeof toStartOfTimelineOrOpts === "object") { ({ toStartOfTimeline, roomState, @@ -379,22 +339,21 @@ } else if (toStartOfTimelineOrOpts !== undefined) { // Deprecation warning // FIXME: Remove after 2023-06-01 (technical debt) - _logger.logger.warn('Overload deprecated: ' + '`EventTimeline.addEvent(event, toStartOfTimeline, roomState?)` ' + 'is deprecated in favor of the overload with `EventTimeline.addEvent(event, IAddEventOptions)`'); + _logger.logger.warn("Overload deprecated: " + "`EventTimeline.addEvent(event, toStartOfTimeline, roomState?)` " + "is deprecated in favor of the overload with `EventTimeline.addEvent(event, IAddEventOptions)`"); } - if (!roomState) { roomState = toStartOfTimeline ? this.startState : this.endState; } - const timelineSet = this.getTimelineSet(); - if (timelineSet.room) { - EventTimeline.setEventMetadata(event, roomState, toStartOfTimeline); // modify state but only on unfiltered timelineSets + EventTimeline.setEventMetadata(event, roomState, toStartOfTimeline); + // modify state but only on unfiltered timelineSets if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) { - roomState.setStateEvents([event], { + roomState?.setStateEvents([event], { timelineWasEmpty - }); // it is possible that the act of setting the state event means we + }); + // it is possible that the act of setting the state event means we // can set more metadata (specifically sender/target props), so try // it again if the prop wasn't previously set. It may also mean that // the sender/target is updated (if the event set was a room member event) @@ -404,67 +363,52 @@ // back in time, else we'll set the .sender value for BEFORE the given // member event, whereas we want to set the .sender value for the ACTUAL // member event itself. - - if (!event.sender || event.getType() === "m.room.member" && !toStartOfTimeline) { + if (!event.sender || event.getType() === _event.EventType.RoomMember && !toStartOfTimeline) { EventTimeline.setEventMetadata(event, roomState, toStartOfTimeline); } } } - let insertIndex; - if (toStartOfTimeline) { insertIndex = 0; } else { insertIndex = this.events.length; } - this.events.splice(insertIndex, 0, event); // insert element - if (toStartOfTimeline) { this.baseIndex++; } } + /** * Remove an event from the timeline * - * @param {string} eventId ID of event to be removed - * @return {?MatrixEvent} removed event, or null if not found + * @param eventId - ID of event to be removed + * @returns removed event, or null if not found */ - - removeEvent(eventId) { for (let i = this.events.length - 1; i >= 0; i--) { const ev = this.events[i]; - if (ev.getId() == eventId) { this.events.splice(i, 1); - if (i < this.baseIndex) { this.baseIndex--; } - return ev; } } - return null; } + /** * Return a string to identify this timeline, for debugging * - * @return {string} name for this timeline + * @returns name for this timeline */ - - toString() { return this.name; } - } - exports.EventTimeline = EventTimeline; - _defineProperty(EventTimeline, "BACKWARDS", Direction.Backward); - _defineProperty(EventTimeline, "FORWARDS", Direction.Forward); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline-set.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline-set.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline-set.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline-set.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,37 +4,31 @@ value: true }); exports.EventTimelineSet = exports.DuplicateStrategy = void 0; - var _eventTimeline = require("./event-timeline"); - var _logger = require("../logger"); - var _room = require("./room"); - var _typedEventEmitter = require("./typed-event-emitter"); - var _relationsContainer = require("./relations-container"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const DEBUG = true; -let debuglog; +/* istanbul ignore next */ +let debuglog; if (DEBUG) { // using bind means that we get to keep useful line numbers in the console debuglog = _logger.logger.log.bind(_logger.logger); } else { + /* istanbul ignore next */ debuglog = function () {}; } - let DuplicateStrategy; exports.DuplicateStrategy = DuplicateStrategy; - (function (DuplicateStrategy) { DuplicateStrategy["Ignore"] = "ignore"; DuplicateStrategy["Replace"] = "replace"; })(DuplicateStrategy || (exports.DuplicateStrategy = DuplicateStrategy = {})); - class EventTimelineSet extends _typedEventEmitter.TypedEventEmitter { /** * Construct a set of EventTimeline objects, typically on behalf of a given @@ -57,154 +51,131 @@ *

In order that we can find events from their ids later, we also maintain a * map from event_id to timeline and index. * - * @constructor - * @param {Room=} room - * Room for this timelineSet. May be null for non-room cases, such as the + * @param room - Room for this timelineSet. May be null for non-room cases, such as the * notification timeline. - * @param {Object} opts Options inherited from Room. - * - * @param {boolean} [opts.timelineSupport = false] - * Set to true to enable improved timeline support. - * @param {Object} [opts.filter = null] - * The filter object, if any, for this timelineSet. - * @param {MatrixClient=} client the Matrix client which owns this EventTimelineSet, + * @param opts - Options inherited from Room. + * @param client - the Matrix client which owns this EventTimelineSet, * can be omitted if room is specified. - * @param {Thread=} thread the thread to which this timeline set relates. + * @param thread - the thread to which this timeline set relates. + * @param isThreadTimeline - Whether this timeline set relates to a thread list timeline + * (e.g., All threads or My threads) */ - constructor(room, opts = {}, client, thread) { + constructor(room, opts = {}, client, thread, threadListType = null) { super(); this.room = room; this.thread = thread; - + this.threadListType = threadListType; _defineProperty(this, "relations", void 0); - _defineProperty(this, "timelineSupport", void 0); - _defineProperty(this, "displayPendingEvents", void 0); - _defineProperty(this, "liveTimeline", void 0); - _defineProperty(this, "timelines", void 0); - _defineProperty(this, "_eventIdToTimeline", new Map()); - _defineProperty(this, "filter", void 0); - this.timelineSupport = Boolean(opts.timelineSupport); this.liveTimeline = new _eventTimeline.EventTimeline(this); - this.displayPendingEvents = opts.pendingEvents !== false; // just a list - *not* ordered. + this.displayPendingEvents = opts.pendingEvents !== false; + // just a list - *not* ordered. this.timelines = [this.liveTimeline]; this._eventIdToTimeline = new Map(); this.filter = opts.filter; this.relations = this.room?.relations ?? new _relationsContainer.RelationsContainer(room?.client ?? client); } + /** * Get all the timelines in this set - * @return {module:models/event-timeline~EventTimeline[]} the timelines in this set + * @returns the timelines in this set */ - - getTimelines() { return this.timelines; } + /** * Get the filter object this timeline set is filtered on, if any - * @return {?Filter} the optional filter for this timelineSet + * @returns the optional filter for this timelineSet */ - - getFilter() { return this.filter; } + /** * Set the filter object this timeline set is filtered on * (passed to the server when paginating via /messages). - * @param {Filter} filter the filter for this timelineSet + * @param filter - the filter for this timelineSet */ - - setFilter(filter) { this.filter = filter; } + /** * Get the list of pending sent events for this timelineSet's room, filtered * by the timelineSet's filter if appropriate. * - * @return {module:models/event.MatrixEvent[]} A list of the sent events + * @returns A list of the sent events * waiting for remote echo. * - * @throws If opts.pendingEventOrdering was not 'detached' + * @throws If `opts.pendingEventOrdering` was not 'detached' */ - - getPendingEvents() { if (!this.room || !this.displayPendingEvents) { return []; } - return this.room.getPendingEvents(); } /** * Get the live timeline for this room. * - * @return {module:models/event-timeline~EventTimeline} live timeline + * @returns live timeline */ - - getLiveTimeline() { return this.liveTimeline; } + /** * Set the live timeline for this room. * - * @return {module:models/event-timeline~EventTimeline} live timeline + * @returns live timeline */ - - setLiveTimeline(timeline) { this.liveTimeline = timeline; } + /** * Return the timeline (if any) this event is in. - * @param {String} eventId the eventId being sought - * @return {module:models/event-timeline~EventTimeline} timeline + * @param eventId - the eventId being sought + * @returns timeline */ - - eventIdToTimeline(eventId) { return this._eventIdToTimeline.get(eventId); } + /** * Track a new event as if it were in the same timeline as an old event, * replacing it. - * @param {String} oldEventId event ID of the original event - * @param {String} newEventId event ID of the replacement event + * @param oldEventId - event ID of the original event + * @param newEventId - event ID of the replacement event */ - - replaceEventId(oldEventId, newEventId) { const existingTimeline = this._eventIdToTimeline.get(oldEventId); - if (existingTimeline) { this._eventIdToTimeline.delete(oldEventId); - this._eventIdToTimeline.set(newEventId, existingTimeline); } } + /** * Reset the live timeline, and start a new one. * *

This is used when /sync returns a 'limited' timeline. * - * @param {string=} backPaginationToken token for back-paginating the new timeline - * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * @param backPaginationToken - token for back-paginating the new timeline + * @param forwardPaginationToken - token for forward-paginating the old live timeline, * if absent or null, all timelines are reset. * - * @fires module:client~MatrixClient#event:"Room.timelineReset" + * @remarks + * Fires {@link RoomEvent.TimelineReset} */ - - resetLiveTimeline(backPaginationToken, forwardPaginationToken) { // Each EventTimeline has RoomState objects tracking the state at the start // and end of that timeline. The copies at the end of the live timeline are @@ -213,121 +184,115 @@ // current live timeline to the end of the new one and, if necessary, // replace it with a newly created one. We also make a copy for the start // of the new timeline. + // if timeline support is disabled, forget about the old timelines const resetAllTimelines = !this.timelineSupport || !forwardPaginationToken; const oldTimeline = this.liveTimeline; const newTimeline = resetAllTimelines ? oldTimeline.forkLive(_eventTimeline.EventTimeline.FORWARDS) : oldTimeline.fork(_eventTimeline.EventTimeline.FORWARDS); - if (resetAllTimelines) { this.timelines = [newTimeline]; this._eventIdToTimeline = new Map(); } else { this.timelines.push(newTimeline); } - if (forwardPaginationToken) { // Now set the forward pagination token on the old live timeline // so it can be forward-paginated. oldTimeline.setPaginationToken(forwardPaginationToken, _eventTimeline.EventTimeline.FORWARDS); - } // make sure we set the pagination token before firing timelineReset, + } + + // make sure we set the pagination token before firing timelineReset, // otherwise clients which start back-paginating will fail, and then get // stuck without realising that they *can* back-paginate. + newTimeline.setPaginationToken(backPaginationToken ?? null, _eventTimeline.EventTimeline.BACKWARDS); - - newTimeline.setPaginationToken(backPaginationToken, _eventTimeline.EventTimeline.BACKWARDS); // Now we can swap the live timeline to the new one. - + // Now we can swap the live timeline to the new one. this.liveTimeline = newTimeline; this.emit(_room.RoomEvent.TimelineReset, this.room, this, resetAllTimelines); } + /** * Get the timeline which contains the given event, if any * - * @param {string} eventId event ID to look for - * @return {?module:models/event-timeline~EventTimeline} timeline containing + * @param eventId - event ID to look for + * @returns timeline containing * the given event, or null if unknown */ - - getTimelineForEvent(eventId) { + if (eventId === null || eventId === undefined) { + return null; + } const res = this._eventIdToTimeline.get(eventId); - return res === undefined ? null : res; } + /** * Get an event which is stored in our timelines * - * @param {string} eventId event ID to look for - * @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown + * @param eventId - event ID to look for + * @returns the given event, or undefined if unknown */ - - findEventById(eventId) { const tl = this.getTimelineForEvent(eventId); - if (!tl) { return undefined; } - return tl.getEvents().find(function (ev) { return ev.getId() == eventId; }); } + /** * Add a new timeline to this timeline list * - * @return {module:models/event-timeline~EventTimeline} newly-created timeline + * @returns newly-created timeline */ - - addTimeline() { if (!this.timelineSupport) { throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable" + " it."); } - const timeline = new _eventTimeline.EventTimeline(this); this.timelines.push(timeline); return timeline; } + /** * Add events to a timeline * *

Will fire "Room.timeline" for each event added. * - * @param {MatrixEvent[]} events A list of events to add. + * @param events - A list of events to add. * - * @param {boolean} toStartOfTimeline True to add these events to the start + * @param toStartOfTimeline - True to add these events to the start * (oldest) instead of the end (newest) of the timeline. If true, the oldest * event will be the last element of 'events'. * - * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * @param timeline - timeline to * add events to. * - * @param {string=} paginationToken token for the next batch of events + * @param paginationToken - token for the next batch of events * - * @fires module:client~MatrixClient#event:"Room.timeline" + * @remarks + * Fires {@link RoomEvent.Timeline} * */ - - addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken) { if (!timeline) { throw new Error("'timeline' not specified for EventTimelineSet.addEventsToTimeline"); } - if (!toStartOfTimeline && timeline == this.liveTimeline) { throw new Error("EventTimelineSet.addEventsToTimeline cannot be used for adding events to " + "the live timeline - use Room.addLiveEvents instead"); } - if (this.filter) { events = this.filter.filterRoomTimeline(events); - if (!events.length) { return; } } - const direction = toStartOfTimeline ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS; - const inverseDirection = toStartOfTimeline ? _eventTimeline.EventTimeline.FORWARDS : _eventTimeline.EventTimeline.BACKWARDS; // Adding events to timelines can be quite complicated. The following + const inverseDirection = toStartOfTimeline ? _eventTimeline.EventTimeline.FORWARDS : _eventTimeline.EventTimeline.BACKWARDS; + + // Adding events to timelines can be quite complicated. The following // illustrates some of the corner-cases. // // Let's say we start by knowing about four timelines. timeline3 and @@ -398,13 +363,9 @@ let didUpdate = false; let lastEventWasNew = false; - - for (let i = 0; i < events.length; i++) { - const event = events[i]; + for (const event of events) { const eventId = event.getId(); - const existingTimeline = this._eventIdToTimeline.get(eventId); - if (!existingTimeline) { // we don't know about this event yet. Just add it to the timeline. this.addEventToTimeline(event, timeline, { @@ -414,16 +375,12 @@ didUpdate = true; continue; } - lastEventWasNew = false; - if (existingTimeline == timeline) { debuglog("Event " + eventId + " already in timeline " + timeline); continue; } - const neighbour = timeline.getNeighbouringTimeline(direction); - if (neighbour) { // this timeline already has a neighbour in the relevant direction; // let's assume the timelines are already correctly linked up, and @@ -439,31 +396,27 @@ } else { debuglog("Event " + eventId + " already in a different " + "timeline " + existingTimeline); } - timeline = existingTimeline; continue; - } // time to join the timelines. - - - _logger.logger.info("Already have timeline for " + eventId + " - joining timeline " + timeline + " to " + existingTimeline); // Variables to keep the line length limited below. + } + // time to join the timelines. + _logger.logger.info("Already have timeline for " + eventId + " - joining timeline " + timeline + " to " + existingTimeline); + // Variables to keep the line length limited below. const existingIsLive = existingTimeline === this.liveTimeline; const timelineIsLive = timeline === this.liveTimeline; const backwardsIsLive = direction === _eventTimeline.EventTimeline.BACKWARDS && existingIsLive; const forwardsIsLive = direction === _eventTimeline.EventTimeline.FORWARDS && timelineIsLive; - if (backwardsIsLive || forwardsIsLive) { // The live timeline should never be spliced into a non-live position. // We use independent logging to better discover the problem at a glance. if (backwardsIsLive) { _logger.logger.warn("Refusing to set a preceding existingTimeLine on our " + "timeline as the existingTimeLine is live (" + existingTimeline + ")"); } - if (forwardsIsLive) { _logger.logger.warn("Refusing to set our preceding timeline on a existingTimeLine " + "as our timeline is live (" + timeline + ")"); } - continue; // abort splicing - try next event } @@ -471,40 +424,35 @@ existingTimeline.setNeighbouringTimeline(timeline, inverseDirection); timeline = existingTimeline; didUpdate = true; - } // see above - if the last event was new to us, or if we didn't find any + } + + // see above - if the last event was new to us, or if we didn't find any // new information, we update the pagination token for whatever // timeline we ended up on. - - if (lastEventWasNew || !didUpdate) { if (direction === _eventTimeline.EventTimeline.FORWARDS && timeline === this.liveTimeline) { _logger.logger.warn({ lastEventWasNew, didUpdate }); // for debugging - - _logger.logger.warn(`Refusing to set forwards pagination token of live timeline ` + `${timeline} to ${paginationToken}`); - return; } - - timeline.setPaginationToken(paginationToken, direction); + timeline.setPaginationToken(paginationToken ?? null, direction); } } + /** * Add an event to the end of this live timeline. * - * @param {MatrixEvent} event Event to be added - * @param {IAddLiveEventOptions} options addLiveEvent options + * @param event - Event to be added + * @param options - addLiveEvent options */ - addLiveEvent(event, duplicateStrategyOrOpts, fromCache = false, roomState) { let duplicateStrategy = duplicateStrategyOrOpts || DuplicateStrategy.Ignore; let timelineWasEmpty; - - if (typeof duplicateStrategyOrOpts === 'object') { + if (typeof duplicateStrategyOrOpts === "object") { ({ duplicateStrategy = DuplicateStrategy.Ignore, fromCache = false, @@ -514,45 +462,37 @@ } else if (duplicateStrategyOrOpts !== undefined) { // Deprecation warning // FIXME: Remove after 2023-06-01 (technical debt) - _logger.logger.warn('Overload deprecated: ' + '`EventTimelineSet.addLiveEvent(event, duplicateStrategy?, fromCache?, roomState?)` ' + 'is deprecated in favor of the overload with ' + '`EventTimelineSet.addLiveEvent(event, IAddLiveEventOptions)`'); + _logger.logger.warn("Overload deprecated: " + "`EventTimelineSet.addLiveEvent(event, duplicateStrategy?, fromCache?, roomState?)` " + "is deprecated in favor of the overload with " + "`EventTimelineSet.addLiveEvent(event, IAddLiveEventOptions)`"); } - if (this.filter) { const events = this.filter.filterRoomTimeline([event]); - if (!events.length) { return; } } - const timeline = this._eventIdToTimeline.get(event.getId()); - if (timeline) { if (duplicateStrategy === DuplicateStrategy.Replace) { debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId()); const tlEvents = timeline.getEvents(); - for (let j = 0; j < tlEvents.length; j++) { if (tlEvents[j].getId() === event.getId()) { // still need to set the right metadata on this event if (!roomState) { roomState = timeline.getState(_eventTimeline.EventTimeline.FORWARDS); } - _eventTimeline.EventTimeline.setEventMetadata(event, roomState, false); + tlEvents[j] = event; - tlEvents[j] = event; // XXX: we need to fire an event when this happens. - + // XXX: we need to fire an event when this happens. break; } } } else { debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " + event.getId()); } - return; } - this.addEventToTimeline(event, this.liveTimeline, { toStartOfTimeline: false, fromCache, @@ -560,25 +500,23 @@ timelineWasEmpty }); } + /** * Add event to the given timeline, and emit Room.timeline. Assumes * we have already checked we don't know about this event. * * Will fire "Room.timeline" for each event added. * - * @param {MatrixEvent} event - * @param {EventTimeline} timeline - * @param {IAddEventToTimelineOptions} options addEventToTimeline options + * @param options - addEventToTimeline options * - * @fires module:client~MatrixClient#event:"Room.timeline" + * @remarks + * Fires {@link RoomEvent.Timeline} */ - addEventToTimeline(event, timeline, toStartOfTimelineOrOpts, fromCache = false, roomState) { let toStartOfTimeline = !!toStartOfTimelineOrOpts; let timelineWasEmpty; - - if (typeof toStartOfTimelineOrOpts === 'object') { + if (typeof toStartOfTimelineOrOpts === "object") { ({ toStartOfTimeline, fromCache = false, @@ -588,18 +526,33 @@ } else if (toStartOfTimelineOrOpts !== undefined) { // Deprecation warning // FIXME: Remove after 2023-06-01 (technical debt) - _logger.logger.warn('Overload deprecated: ' + '`EventTimelineSet.addEventToTimeline(event, timeline, toStartOfTimeline, fromCache?, roomState?)` ' + 'is deprecated in favor of the overload with ' + '`EventTimelineSet.addEventToTimeline(event, timeline, IAddEventToTimelineOptions)`'); + _logger.logger.warn("Overload deprecated: " + "`EventTimelineSet.addEventToTimeline(event, timeline, toStartOfTimeline, fromCache?, roomState?)` " + "is deprecated in favor of the overload with " + "`EventTimelineSet.addEventToTimeline(event, timeline, IAddEventToTimelineOptions)`"); + } + if (timeline.getTimelineSet() !== this) { + throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " + + "in timelineSet(threadId=${this.thread?.id})`); + } + + // Make sure events don't get mixed in timelines they shouldn't be in (e.g. a + // threaded message should not be in the main timeline). + // + // We can only run this check for timelines with a `room` because `canContain` + // requires it + if (this.room && !this.canContain(event)) { + let eventDebugString = `event=${event.getId()}`; + if (event.threadRootId) { + eventDebugString += `(belongs to thread=${event.threadRootId})`; + } + _logger.logger.warn(`EventTimelineSet.addEventToTimeline: Ignoring ${eventDebugString} that does not belong ` + `in timeline=${timeline.toString()} timelineSet(threadId=${this.thread?.id})`); + return; } - const eventId = event.getId(); timeline.addEvent(event, { toStartOfTimeline, roomState, timelineWasEmpty }); - this._eventIdToTimeline.set(eventId, timeline); - this.relations.aggregateParentEvent(event); this.relations.aggregateChildEvent(event, this); const data = { @@ -608,25 +561,23 @@ }; this.emit(_room.RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data); } + /** * Replaces event with ID oldEventId with one with newEventId, if oldEventId is * recognised. Otherwise, add to the live timeline. Used to handle remote echos. * - * @param {MatrixEvent} localEvent the new event to be added to the timeline - * @param {String} oldEventId the ID of the original event - * @param {boolean} newEventId the ID of the replacement event + * @param localEvent - the new event to be added to the timeline + * @param oldEventId - the ID of the original event + * @param newEventId - the ID of the replacement event * - * @fires module:client~MatrixClient#event:"Room.timeline" + * @remarks + * Fires {@link RoomEvent.Timeline} */ - - handleRemoteEcho(localEvent, oldEventId, newEventId) { // XXX: why don't we infer newEventId from localEvent? const existingTimeline = this._eventIdToTimeline.get(oldEventId); - if (existingTimeline) { this._eventIdToTimeline.delete(oldEventId); - this._eventIdToTimeline.set(newEventId, existingTimeline); } else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) { this.addEventToTimeline(localEvent, this.liveTimeline, { @@ -634,118 +585,99 @@ }); } } + /** * Removes a single event from this room. * - * @param {String} eventId The id of the event to remove + * @param eventId - The id of the event to remove * - * @return {?MatrixEvent} the removed event, or null if the event was not found + * @returns the removed event, or null if the event was not found * in this room. */ - - removeEvent(eventId) { const timeline = this._eventIdToTimeline.get(eventId); - if (!timeline) { return null; } - const removed = timeline.removeEvent(eventId); - if (removed) { this._eventIdToTimeline.delete(eventId); - const data = { timeline: timeline }; this.emit(_room.RoomEvent.Timeline, removed, this.room, undefined, true, data); } - return removed; } + /** * Determine where two events appear in the timeline relative to one another * - * @param {string} eventId1 The id of the first event - * @param {string} eventId2 The id of the second event - * @return {?number} a number less than zero if eventId1 precedes eventId2, and + * @param eventId1 - The id of the first event + * @param eventId2 - The id of the second event + * @returns a number less than zero if eventId1 precedes eventId2, and * greater than zero if eventId1 succeeds eventId2. zero if they are the * same event; null if we can't tell (either because we don't know about one * of the events, or because they are in separate timelines which don't join * up). */ - - compareEventOrdering(eventId1, eventId2) { if (eventId1 == eventId2) { // optimise this case return 0; } - const timeline1 = this._eventIdToTimeline.get(eventId1); - const timeline2 = this._eventIdToTimeline.get(eventId2); - if (timeline1 === undefined) { return null; } - if (timeline2 === undefined) { return null; } - if (timeline1 === timeline2) { - // both events are in the same timeline - figure out their - // relative indices - let idx1; - let idx2; + // both events are in the same timeline - figure out their relative indices + let idx1 = undefined; + let idx2 = undefined; const events = timeline1.getEvents(); - for (let idx = 0; idx < events.length && (idx1 === undefined || idx2 === undefined); idx++) { const evId = events[idx].getId(); - if (evId == eventId1) { idx1 = idx; } - if (evId == eventId2) { idx2 = idx; } } - return idx1 - idx2; - } // the events are in different timelines. Iterate through the - // linkedlist to see which comes first. - // first work forwards from timeline1 + } + // the events are in different timelines. Iterate through the + // linkedlist to see which comes first. + // first work forwards from timeline1 let tl = timeline1; - while (tl) { if (tl === timeline2) { // timeline1 is before timeline2 return -1; } - tl = tl.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS); - } // now try backwards from timeline1 - + } + // now try backwards from timeline1 tl = timeline1; - while (tl) { if (tl === timeline2) { // timeline2 is before timeline1 return 1; } - tl = tl.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS); - } // the timelines are not contiguous. - + } + // the timelines are not contiguous. return null; } + /** * Determine whether a given event can sanely be added to this event timeline set, * for timeline sets relating to a thread, only return true for events in the same @@ -753,69 +685,22 @@ * for events which should be shown in the main room timeline. * Requires the `room` property to have been set at EventTimelineSet construction time. * - * @param event {MatrixEvent} the event to check whether it belongs to this timeline set. - * @throws {Error} if `room` was not set when constructing this timeline set. - * @return {boolean} whether the event belongs to this timeline set. + * @param event - the event to check whether it belongs to this timeline set. + * @throws Error if `room` was not set when constructing this timeline set. + * @returns whether the event belongs to this timeline set. */ - - canContain(event) { if (!this.room) { throw new Error("Cannot call `EventTimelineSet::canContain without a `room` set. " + "Set the room when creating the EventTimelineSet to call this method."); } - const { threadId, shouldLiveInRoom } = this.room.eventShouldLiveIn(event); - if (this.thread) { return this.thread.id === threadId; } - return shouldLiveInRoom; } - } -/** - * Fires whenever the timeline in a room is updated. - * @event module:client~MatrixClient#"Room.timeline" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {?Room} room The room, if any, whose timeline was updated. - * @param {boolean} toStartOfTimeline True if this event was added to the start - * @param {boolean} removed True if this event has just been removed from the timeline - * (beginning; oldest) of the timeline e.g. due to pagination. - * - * @param {object} data more data about the event - * - * @param {module:models/event-timeline.EventTimeline} data.timeline the timeline the - * event was added to/removed from - * - * @param {boolean} data.liveEvent true if the event was a real-time event - * added to the end of the live timeline - * - * @example - * matrixClient.on("Room.timeline", - * function(event, room, toStartOfTimeline, removed, data) { - * if (!toStartOfTimeline && data.liveEvent) { - * var messageToAppend = room.timeline.[room.timeline.length - 1]; - * } - * }); - */ - -/** - * Fires whenever the live timeline in a room is reset. - * - * When we get a 'limited' sync (for example, after a network outage), we reset - * the live timeline to be empty before adding the recent events to the new - * timeline. This event is fired after the timeline is reset, and before the - * new events are added. - * - * @event module:client~MatrixClient#"Room.timelineReset" - * @param {Room} room The room whose live timeline was reset, if any - * @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset - * @param {boolean} resetAllTimelines True if all timelines were reset. - */ - - exports.EventTimelineSet = EventTimelineSet; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/invites-ignorer.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/invites-ignorer.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/invites-ignorer.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/invites-ignorer.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,15 +4,10 @@ value: true }); exports.PolicyScope = exports.POLICIES_ACCOUNT_EVENT_TYPE = exports.IgnoredInvites = exports.IGNORE_INVITES_ACCOUNT_EVENT_KEY = void 0; - var _matrixEventsSdk = require("matrix-events-sdk"); - var _eventTimeline = require("./event-timeline"); - var _partials = require("../@types/partials"); - var _utils = require("../utils"); - /* Copyright 2022 The Matrix.org Foundation C.I.C. @@ -28,26 +23,27 @@ See the License for the specific language governing permissions and limitations under the License. */ + /// The event type storing the user's individual policies. /// /// Exported for testing purposes. -const POLICIES_ACCOUNT_EVENT_TYPE = new _matrixEventsSdk.UnstableValue("m.policies", "org.matrix.msc3847.policies"); /// The key within the user's individual policies storing the user's ignored invites. +const POLICIES_ACCOUNT_EVENT_TYPE = new _matrixEventsSdk.UnstableValue("m.policies", "org.matrix.msc3847.policies"); + +/// The key within the user's individual policies storing the user's ignored invites. /// /// Exported for testing purposes. - exports.POLICIES_ACCOUNT_EVENT_TYPE = POLICIES_ACCOUNT_EVENT_TYPE; -const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new _matrixEventsSdk.UnstableValue("m.ignore.invites", "org.matrix.msc3847.ignore.invites"); /// The types of recommendations understood. +const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new _matrixEventsSdk.UnstableValue("m.ignore.invites", "org.matrix.msc3847.ignore.invites"); +/// The types of recommendations understood. exports.IGNORE_INVITES_ACCOUNT_EVENT_KEY = IGNORE_INVITES_ACCOUNT_EVENT_KEY; var PolicyRecommendation; /** * The various scopes for policies. */ - (function (PolicyRecommendation) { PolicyRecommendation["Ban"] = "m.ban"; })(PolicyRecommendation || (PolicyRecommendation = {})); - let PolicyScope; /** * A container for ignored invites. @@ -59,29 +55,25 @@ * applications turn out to require longer lists, we may need to rework * our data structures. */ - exports.PolicyScope = PolicyScope; - (function (PolicyScope) { PolicyScope["User"] = "m.policy.user"; PolicyScope["Room"] = "m.policy.room"; PolicyScope["Server"] = "m.policy.server"; })(PolicyScope || (exports.PolicyScope = PolicyScope = {})); - class IgnoredInvites { constructor(client) { this.client = client; } + /** * Add a new rule. * - * @param scope The scope for this rule. - * @param entity The entity covered by this rule. Globs are supported. - * @param reason A human-readable reason for introducing this new rule. - * @return The event id for the new rule. + * @param scope - The scope for this rule. + * @param entity - The entity covered by this rule. Globs are supported. + * @param reason - A human-readable reason for introducing this new rule. + * @returns The event id for the new rule. */ - - async addRule(scope, entity, reason) { const target = await this.getOrCreateTargetRoom(); const response = await this.client.sendStateEvent(target.roomId, scope, { @@ -91,21 +83,21 @@ }); return response.event_id; } + /** * Remove a rule. */ - - async removeRule(event) { await this.client.redactEvent(event.getRoomId(), event.getId()); } + /** * Add a new room to the list of sources. If the user isn't a member of the * room, attempt to join it. * - * @param roomId A valid room id. If this room is already in the list + * @param roomId - A valid room id. If this room is already in the list * of sources, it will not be duplicated. - * @return `true` if the source was added, `false` if it was already present. + * @returns `true` if the source was added, `false` if it was already present. * @throws If `roomId` isn't the id of a room that the current user is already * member of or can join. * @@ -115,36 +107,32 @@ * This rewrite is inherently racy and could overwrite or be overwritten by * other concurrent rewrites of the same object. */ - - async addSource(roomId) { // We attempt to join the room *before* calling // `await this.getOrCreateSourceRooms()` to decrease the duration // of the racy section. - await this.client.joinRoom(roomId); // Race starts. - + await this.client.joinRoom(roomId); + // Race starts. const sources = (await this.getOrCreateSourceRooms()).map(room => room.roomId); - if (sources.includes(roomId)) { return false; } - sources.push(roomId); await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => { ignoreInvitesPolicies.sources = sources; - }); // Race ends. + }); + // Race ends. return true; } + /** * Find out whether an invite should be ignored. * - * @param sender The user id for the user who issued the invite. - * @param roomId The room to which the user is invited. + * @param sender - The user id for the user who issued the invite. + * @param roomId - The room to which the user is invited. * @returns A rule matching the entity, if any was found, `null` otherwise. */ - - async getRuleForInvite({ sender, roomId @@ -162,10 +150,8 @@ const policyRooms = await this.getOrCreateSourceRooms(); const senderServer = sender.split(":")[1]; const roomServer = roomId.split(":")[1]; - for (const room of policyRooms) { const state = room.getUnfilteredTimelineSet().getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); - for (const { scope, entities @@ -180,43 +166,37 @@ entities: [senderServer, roomServer] }]) { const events = state.getStateEvents(scope); - for (const event of events) { const content = event.getContent(); - if (content?.recommendation != PolicyRecommendation.Ban) { // Ignoring invites only looks at `m.ban` recommendations. continue; } - const glob = content?.entity; - if (!glob) { // Invalid event. continue; } - let regexp; - try { regexp = new RegExp((0, _utils.globToRegexp)(glob, false)); } catch (ex) { // Assume invalid event. continue; } - for (const entity of entities) { if (entity && regexp.test(entity)) { return event; } - } // No match. - + } + // No match. } } } return null; } + /** * Get the target room, i.e. the room in which any new rule should be written. * @@ -231,39 +211,36 @@ * This rewrite is inherently racy and could overwrite or be overwritten by * other concurrent rewrites of the same object. */ - - async getOrCreateTargetRoom() { const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies(); - let target = ignoreInvitesPolicies.target; // Validate `target`. If it is invalid, trash out the current `target` + let target = ignoreInvitesPolicies.target; + // Validate `target`. If it is invalid, trash out the current `target` // and create a new room. - if (typeof target !== "string") { target = null; } - if (target) { // Check that the room exists and is valid. const room = this.client.getRoom(target); - if (room) { return room; } else { target = null; } - } // We need to create our own policy room for ignoring invites. - - + } + // We need to create our own policy room for ignoring invites. target = (await this.client.createRoom({ name: "Individual Policy Room", preset: _partials.Preset.PrivateChat })).room_id; await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => { ignoreInvitesPolicies.target = target; - }); // Since we have just called `createRoom`, `getRoom` should not be `null`. + }); + // Since we have just called `createRoom`, `getRoom` should not be `null`. return this.client.getRoom(target); } + /** * Get the list of source rooms, i.e. the rooms from which rules need to be read. * @@ -278,28 +255,24 @@ * This rewrite is inherently racy and could overwrite or be overwritten by * other concurrent rewrites of the same object. */ - - async getOrCreateSourceRooms() { const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies(); - let sources = ignoreInvitesPolicies.sources; // Validate `sources`. If it is invalid, trash out the current `sources` - // and create a new list of sources from `target`. + let sources = ignoreInvitesPolicies.sources; + // Validate `sources`. If it is invalid, trash out the current `sources` + // and create a new list of sources from `target`. let hasChanges = false; - if (!Array.isArray(sources)) { // `sources` could not be an array. hasChanges = true; sources = []; } - - let sourceRooms = sources // `sources` could contain non-string / invalid room ids + let sourceRooms = sources + // `sources` could contain non-string / invalid room ids .filter(roomId => typeof roomId === "string").map(roomId => this.client.getRoom(roomId)).filter(room => !!room); - if (sourceRooms.length != sources.length) { hasChanges = true; } - if (sourceRooms.length == 0) { // `sources` could be empty (possibly because we've removed // invalid content) @@ -307,7 +280,6 @@ hasChanges = true; sourceRooms = [target]; } - if (hasChanges) { // Reload `policies`/`ignoreInvitesPolicies` in case it has been changed // during or by our call to `this.getTargetRoom()`. @@ -315,9 +287,9 @@ ignoreInvitesPolicies.sources = sources; }); } - return sourceRooms; } + /** * Fetch the `IGNORE_INVITES_POLICIES` object from account data. * @@ -328,16 +300,13 @@ * * @returns A non-null object. */ - - getIgnoreInvitesPolicies() { return this.getPoliciesAndIgnoreInvitesPolicies().ignoreInvitesPolicies; } + /** * Modify in place the `IGNORE_INVITES_POLICIES` object from account data. */ - - async withIgnoreInvitesPolicies(cb) { const { policies, @@ -347,55 +316,43 @@ policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies; await this.client.setAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name, policies); } + /** * As `getIgnoreInvitesPolicies` but also return the `POLICIES_ACCOUNT_EVENT_TYPE` * object. */ - - getPoliciesAndIgnoreInvitesPolicies() { let policies = {}; - for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) { if (!key) { continue; } - const value = this.client.getAccountData(key)?.getContent(); - if (value) { policies = value; break; } } - let ignoreInvitesPolicies = {}; let hasIgnoreInvitesPolicies = false; - for (const key of [IGNORE_INVITES_ACCOUNT_EVENT_KEY.name, IGNORE_INVITES_ACCOUNT_EVENT_KEY.altName]) { if (!key) { continue; } - const value = policies[key]; - if (value && typeof value == "object") { ignoreInvitesPolicies = value; hasIgnoreInvitesPolicies = true; break; } } - if (!hasIgnoreInvitesPolicies) { policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies; } - return { policies, ignoreInvitesPolicies }; } - } - exports.IgnoredInvites = IgnoredInvites; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089Branch.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089Branch.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089Branch.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089Branch.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,222 +4,201 @@ value: true }); exports.MSC3089Branch = void 0; - var _event = require("../@types/event"); - var _eventTimeline = require("./event-timeline"); - function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference * to a file (leaf) in the tree. Note that this is UNSTABLE and subject to breaking changes * without notice. */ class MSC3089Branch { - constructor(client, indexEvent, directory) {// Nothing to do - + constructor(client, indexEvent, directory) { this.client = client; this.indexEvent = indexEvent; this.directory = directory; - } + } // Nothing to do + /** * The file ID. */ - - get id() { const stateKey = this.indexEvent.getStateKey(); - if (!stateKey) { throw new Error("State key not found for branch"); } - return stateKey; } + /** * Whether this branch is active/valid. */ - - get isActive() { return this.indexEvent.getContent()["active"] === true; } + /** * Version for the file, one-indexed. */ - - get version() { return this.indexEvent.getContent()["version"] ?? 1; } - get roomId() { return this.indexEvent.getRoomId(); } + /** * Deletes the file from the tree, including all prior edits/versions. - * @returns {Promise} Resolves when complete. + * @returns Promise which resolves when complete. */ - - async delete() { await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, {}, this.id); await this.client.redactEvent(this.roomId, this.id); const nextVersion = (await this.getVersionHistory())[1]; // [0] will be us - if (nextVersion) await nextVersion.delete(); // implicit recursion } + /** * Gets the name for this file. - * @returns {string} The name, or "Unnamed File" if unknown. + * @returns The name, or "Unnamed File" if unknown. */ - - getName() { - return this.indexEvent.getContent()['name'] || "Unnamed File"; + return this.indexEvent.getContent()["name"] || "Unnamed File"; } + /** * Sets the name for this file. - * @param {string} name The new name for this file. - * @returns {Promise} Resolves when complete. + * @param name - The new name for this file. + * @returns Promise which resolves when complete. */ - - async setName(name) { await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, _objectSpread(_objectSpread({}, this.indexEvent.getContent()), {}, { name: name }), this.id); } + /** * Gets whether or not a file is locked. - * @returns {boolean} True if locked, false otherwise. + * @returns True if locked, false otherwise. */ - - isLocked() { - return this.indexEvent.getContent()['locked'] || false; + return this.indexEvent.getContent()["locked"] || false; } + /** * Sets a file as locked or unlocked. - * @param {boolean} locked True to lock the file, false otherwise. - * @returns {Promise} Resolves when complete. + * @param locked - True to lock the file, false otherwise. + * @returns Promise which resolves when complete. */ - - async setLocked(locked) { await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, _objectSpread(_objectSpread({}, this.indexEvent.getContent()), {}, { locked: locked }), this.id); } + /** * Gets information about the file needed to download it. - * @returns {Promise<{info: IEncryptedFile, httpUrl: string}>} Information about the file. + * @returns Information about the file. */ - - async getFileInfo() { const event = await this.getFileEvent(); - const file = event.getOriginalContent()['file']; - const httpUrl = this.client.mxcUrlToHttp(file['url']); - + const file = event.getOriginalContent()["file"]; + const httpUrl = this.client.mxcUrlToHttp(file["url"]); if (!httpUrl) { - throw new Error(`No HTTP URL available for ${file['url']}`); + throw new Error(`No HTTP URL available for ${file["url"]}`); } - return { info: file, httpUrl: httpUrl }; } + /** * Gets the event the file points to. - * @returns {Promise} Resolves to the file's event. + * @returns Promise which resolves to the file's event. */ - - async getFileEvent() { const room = this.client.getRoom(this.roomId); if (!room) throw new Error("Unknown room"); - let event = room.getUnfilteredTimelineSet().findEventById(this.id); // keep scrolling back if needed until we find the event or reach the start of the room: + let event = room.getUnfilteredTimelineSet().findEventById(this.id); + // keep scrolling back if needed until we find the event or reach the start of the room: while (!event && room.getLiveTimeline().getState(_eventTimeline.EventTimeline.BACKWARDS).paginationToken) { await this.client.scrollback(room, 100); event = room.getUnfilteredTimelineSet().findEventById(this.id); } + if (!event) throw new Error("Failed to find event"); - if (!event) throw new Error("Failed to find event"); // Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true` + // Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true` // to ensure that the relations system in the sdk will function. - await this.client.decryptEventIfNeeded(event, { emit: true, isRetry: true }); return event; } + /** * Creates a new version of this file with contents in a type that is compatible with MatrixClient.uploadContent(). - * @param {string} name The name of the file. - * @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents. - * @param {Partial} info The encrypted file information. - * @param {IContent} additionalContent Optional event content fields to include in the message. - * @returns {Promise} Resolves to the file event's sent response. + * @param name - The name of the file. + * @param encryptedContents - The encrypted contents. + * @param info - The encrypted file information. + * @param additionalContent - Optional event content fields to include in the message. + * @returns Promise which resolves to the file event's sent response. */ - - async createNewVersion(name, encryptedContents, info, additionalContent) { const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, _objectSpread(_objectSpread({}, additionalContent ?? {}), {}, { "m.new_content": true, "m.relates_to": { - "rel_type": _event.RelationType.Replace, - "event_id": this.id + rel_type: _event.RelationType.Replace, + event_id: this.id } - })); // Update the version of the new event + })); + // Update the version of the new event await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, { active: true, name: name, version: this.version + 1 - }, fileEventResponse['event_id']); // Deprecate ourselves + }, fileEventResponse["event_id"]); + // Deprecate ourselves await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, _objectSpread(_objectSpread({}, this.indexEvent.getContent()), {}, { active: false }), this.id); return fileEventResponse; } + /** * Gets the file's version history, starting at this file. - * @returns {Promise} Resolves to the file's version history, with the + * @returns Promise which resolves to the file's version history, with the * first element being the current version and the last element being the first version. */ - - async getVersionHistory() { const fileHistory = []; fileHistory.push(this); // start with ourselves const room = this.client.getRoom(this.roomId); - if (!room) throw new Error("Invalid or unknown room"); // Clone the timeline to reverse it, getting most-recent-first ordering, hopefully + if (!room) throw new Error("Invalid or unknown room"); + + // Clone the timeline to reverse it, getting most-recent-first ordering, hopefully // shortening the awful loop below. Without the clone, we can unintentionally mutate // the timeline. + const timelineEvents = [...room.getLiveTimeline().getEvents()].reverse(); - const timelineEvents = [...room.getLiveTimeline().getEvents()].reverse(); // XXX: This is a very inefficient search, but it's the best we can do with the + // XXX: This is a very inefficient search, but it's the best we can do with the // relations structure we have in the SDK. As of writing, it is not worth the // investment in improving the structure. - let childEvent; let parentEvent = await this.getFileEvent(); - do { childEvent = timelineEvents.find(e => e.replacingEventId() === parentEvent.getId()); - if (childEvent) { const branch = this.directory.getFile(childEvent.getId()); - if (branch) { fileHistory.push(branch); parentEvent = childEvent; @@ -228,10 +207,7 @@ } } } while (childEvent); - return fileHistory; } - } - exports.MSC3089Branch = MSC3089Branch; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089TreeSpace.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089TreeSpace.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089TreeSpace.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089TreeSpace.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,27 +4,18 @@ value: true }); exports.TreePermissions = exports.MSC3089TreeSpace = exports.DEFAULT_TREE_POWER_LEVELS_TEMPLATE = void 0; - var _pRetry = _interopRequireDefault(require("p-retry")); - var _event = require("../@types/event"); - var _logger = require("../logger"); - var _utils = require("../utils"); - var _MSC3089Branch = require("./MSC3089Branch"); - var _megolm = require("../crypto/algorithms/megolm"); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * The recommended defaults for a tree space's power levels. Note that this * is UNSTABLE and subject to breaking changes without notice. @@ -52,93 +43,81 @@ [_event.EventType.Sticker]: 50 }, users: {} // defined by calling code - }; + /** * Ease-of-use representation for power levels represented as simple roles. * Note that this is UNSTABLE and subject to breaking changes without notice. */ - exports.DEFAULT_TREE_POWER_LEVELS_TEMPLATE = DEFAULT_TREE_POWER_LEVELS_TEMPLATE; -let TreePermissions; +let TreePermissions; // "Admin" or PL100 /** * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) * file tree Space. Note that this is UNSTABLE and subject to breaking changes * without notice. */ - exports.TreePermissions = TreePermissions; - (function (TreePermissions) { TreePermissions["Viewer"] = "viewer"; TreePermissions["Editor"] = "editor"; TreePermissions["Owner"] = "owner"; })(TreePermissions || (exports.TreePermissions = TreePermissions = {})); - class MSC3089TreeSpace { constructor(client, roomId) { this.client = client; this.roomId = roomId; - _defineProperty(this, "room", void 0); - this.room = this.client.getRoom(this.roomId); if (!this.room) throw new Error("Unknown room"); } + /** * Syntactic sugar for room ID of the Space. */ - - get id() { return this.roomId; } + /** * Whether or not this is a top level space. */ - - get isTopLevel() { // XXX: This is absolutely not how you find out if the space is top level // but is safe for a managed usecase like we offer in the SDK. const parentEvents = this.room.currentState.getStateEvents(_event.EventType.SpaceParent); if (!parentEvents?.length) return true; - return parentEvents.every(e => !e.getContent()?.['via']); + return parentEvents.every(e => !e.getContent()?.["via"]); } + /** * Sets the name of the tree space. - * @param {string} name The new name for the space. - * @returns {Promise} Resolves when complete. + * @param name - The new name for the space. + * @returns Promise which resolves when complete. */ - - async setName(name) { await this.client.sendStateEvent(this.roomId, _event.EventType.RoomName, { name }, ""); } + /** * Invites a user to the tree space. They will be given the default Viewer * permission level unless specified elsewhere. - * @param {string} userId The user ID to invite. - * @param {boolean} andSubspaces True (default) to invite the user to all + * @param userId - The user ID to invite. + * @param andSubspaces - True (default) to invite the user to all * directories/subspaces too, recursively. - * @param {boolean} shareHistoryKeys True (default) to share encryption keys + * @param shareHistoryKeys - True (default) to share encryption keys * with the invited user. This will allow them to decrypt the events (files) * in the tree. Keys will not be shared if the room is lacking appropriate * history visibility (by default, history visibility is "shared" in trees, * which is an appropriate visibility for these purposes). - * @returns {Promise} Resolves when complete. + * @returns Promise which resolves when complete. */ - - async invite(userId, andSubspaces = true, shareHistoryKeys = true) { const promises = [this.retryInvite(userId)]; - if (andSubspaces) { promises.push(...this.getDirectories().map(d => d.invite(userId, andSubspaces, shareHistoryKeys))); } - return Promise.all(promises).then(() => { // Note: key sharing is default on because for file trees it is relatively important that the invite // target can actually decrypt the files. The implied use case is that by inviting a user to the tree @@ -150,7 +129,6 @@ } }); } - retryInvite(userId) { return (0, _utils.simpleRetryOperation)(async () => { await this.client.invite(this.roomId, userId).catch(e => { @@ -158,78 +136,69 @@ if (e?.errcode === "M_FORBIDDEN") { throw new _pRetry.default.AbortError(e); } - throw e; }); }); } + /** * Sets the permissions of a user to the given role. Note that if setting a user * to Owner then they will NOT be able to be demoted. If the user does not have * permission to change the power level of the target, an error will be thrown. - * @param {string} userId The user ID to change the role of. - * @param {TreePermissions} role The role to assign. - * @returns {Promise} Resolves when complete. + * @param userId - The user ID to change the role of. + * @param role - The role to assign. + * @returns Promise which resolves when complete. */ - - async setPermissions(userId, role) { const currentPls = this.room.currentState.getStateEvents(_event.EventType.RoomPowerLevels, ""); if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels"); - const pls = currentPls.getContent() || {}; - const viewLevel = pls['users_default'] || 0; - const editLevel = pls['events_default'] || 50; - const adminLevel = pls['events']?.[_event.EventType.RoomPowerLevels] || 100; - const users = pls['users'] || {}; - + const pls = currentPls?.getContent() || {}; + const viewLevel = pls["users_default"] || 0; + const editLevel = pls["events_default"] || 50; + const adminLevel = pls["events"]?.[_event.EventType.RoomPowerLevels] || 100; + const users = pls["users"] || {}; switch (role) { case TreePermissions.Viewer: users[userId] = viewLevel; break; - case TreePermissions.Editor: users[userId] = editLevel; break; - case TreePermissions.Owner: users[userId] = adminLevel; break; - default: throw new Error("Invalid role: " + role); } - - pls['users'] = users; + pls["users"] = users; await this.client.sendStateEvent(this.roomId, _event.EventType.RoomPowerLevels, pls, ""); } + /** * Gets the current permissions of a user. Note that any users missing explicit permissions (or not * in the space) will be considered Viewers. Appropriate membership checks need to be performed * elsewhere. - * @param {string} userId The user ID to check permissions of. - * @returns {TreePermissions} The permissions for the user, defaulting to Viewer. + * @param userId - The user ID to check permissions of. + * @returns The permissions for the user, defaulting to Viewer. */ - - getPermissions(userId) { const currentPls = this.room.currentState.getStateEvents(_event.EventType.RoomPowerLevels, ""); if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels"); - const pls = currentPls.getContent() || {}; - const viewLevel = pls['users_default'] || 0; - const editLevel = pls['events_default'] || 50; - const adminLevel = pls['events']?.[_event.EventType.RoomPowerLevels] || 100; - const userLevel = pls['users']?.[userId] || viewLevel; + const pls = currentPls?.getContent() || {}; + const viewLevel = pls["users_default"] || 0; + const editLevel = pls["events_default"] || 50; + const adminLevel = pls["events"]?.[_event.EventType.RoomPowerLevels] || 100; + const userLevel = pls["users"]?.[userId] || viewLevel; if (userLevel >= adminLevel) return TreePermissions.Owner; if (userLevel >= editLevel) return TreePermissions.Editor; return TreePermissions.Viewer; } + /** * Creates a directory under this tree space, represented as another tree space. - * @param {string} name The name for the directory. - * @returns {Promise} Resolves to the created directory. + * @param name - The name for the directory. + * @returns Promise which resolves to the created directory. */ - - async createDirectory(name) { const directory = await this.client.unstableCreateFileTree(name); await this.client.sendStateEvent(this.roomId, _event.EventType.SpaceChild, { @@ -240,20 +209,17 @@ }, this.roomId); return directory; } + /** * Gets a list of all known immediate subdirectories to this tree space. - * @returns {MSC3089TreeSpace[]} The tree spaces (directories). May be empty, but not null. + * @returns The tree spaces (directories). May be empty, but not null. */ - - getDirectories() { const trees = []; const children = this.room.currentState.getStateEvents(_event.EventType.SpaceChild); - for (const child of children) { try { const stateKey = child.getStateKey(); - if (stateKey) { const tree = this.client.unstableGetFileTreeSpace(stateKey); if (tree) trees.push(tree); @@ -262,57 +228,46 @@ _logger.logger.warn("Unable to create tree space instance for listing. Are we joined?", e); } } - return trees; } + /** * Gets a subdirectory of a given ID under this tree space. Note that this will not recurse * into children and instead only look one level deep. - * @param {string} roomId The room ID (directory ID) to find. - * @returns {MSC3089TreeSpace | undefined} The directory, or undefined if not found. + * @param roomId - The room ID (directory ID) to find. + * @returns The directory, or undefined if not found. */ - - getDirectory(roomId) { return this.getDirectories().find(r => r.roomId === roomId); } + /** * Deletes the tree, kicking all members and deleting **all subdirectories**. - * @returns {Promise} Resolves when complete. + * @returns Promise which resolves when complete. */ - - async delete() { const subdirectories = this.getDirectories(); - for (const dir of subdirectories) { await dir.delete(); } - const kickMemberships = ["invite", "knock", "join"]; const members = this.room.currentState.getStateEvents(_event.EventType.RoomMember); - for (const member of members) { const isNotUs = member.getStateKey() !== this.client.getUserId(); - if (isNotUs && kickMemberships.includes(member.getContent().membership)) { const stateKey = member.getStateKey(); - if (!stateKey) { throw new Error("State key not found for branch"); } - await this.client.kick(this.roomId, stateKey, "Room deleted"); } } - await this.client.leave(this.roomId); } - getOrderedChildren(children) { const ordered = children.map(c => ({ roomId: c.getStateKey(), - order: c.getContent()['order'] + order: c.getContent()["order"] })).filter(c => c.roomId); ordered.sort((a, b) => { if (a.order && !b.order) { @@ -322,19 +277,15 @@ } else if (!a.order && !b.order) { const roomA = this.client.getRoom(a.roomId); const roomB = this.client.getRoom(b.roomId); - if (!roomA || !roomB) { // just don't bother trying to do more partial sorting return (0, _utils.lexicographicCompare)(a.roomId, b.roomId); } - const createTsA = roomA.currentState.getStateEvents(_event.EventType.RoomCreate, "")?.getTs() ?? 0; const createTsB = roomB.currentState.getStateEvents(_event.EventType.RoomCreate, "")?.getTs() ?? 0; - if (createTsA === createTsB) { return (0, _utils.lexicographicCompare)(a.roomId, b.roomId); } - return createTsA - createTsB; } else { // both not-null orders @@ -343,27 +294,25 @@ }); return ordered; } - getParentRoom() { const parents = this.room.currentState.getStateEvents(_event.EventType.SpaceParent); const parent = parents[0]; // XXX: Wild assumption + if (!parent) throw new Error("Expected to have a parent in a non-top level space"); - if (!parent) throw new Error("Expected to have a parent in a non-top level space"); // XXX: We are assuming the parent is a valid tree space. + // XXX: We are assuming the parent is a valid tree space. // We probably don't need to validate the parent room state for this usecase though. - const stateKey = parent.getStateKey(); if (!stateKey) throw new Error("No state key found for parent"); const parentRoom = this.client.getRoom(stateKey); if (!parentRoom) throw new Error("Unable to locate room for parent"); return parentRoom; } + /** * Gets the current order index for this directory. Note that if this is the top level space * then -1 will be returned. - * @returns {number} The order index of this space. + * @returns The order index of this space. */ - - getOrder() { if (this.isTopLevel) return -1; const parentRoom = this.getParentRoom(); @@ -371,16 +320,15 @@ const ordered = this.getOrderedChildren(children); return ordered.findIndex(c => c.roomId === this.roomId); } + /** * Sets the order index for this directory within its parent. Note that if this is a top level * space then an error will be thrown. -1 can be used to move the child to the start, and numbers * larger than the number of children can be used to move the child to the end. - * @param {number} index The new order index for this space. - * @returns {Promise} Resolves when complete. + * @param index - The new order index for this space. + * @returns Promise which resolves when complete. * @throws Throws if this is a top level space. */ - - async setOrder(index) { if (this.isTopLevel) throw new Error("Cannot set order of top level spaces currently"); const parentRoom = this.getParentRoom(); @@ -389,18 +337,15 @@ index = Math.max(Math.min(index, ordered.length - 1), 0); const currentIndex = this.getOrder(); const movingUp = currentIndex < index; - if (movingUp && index === ordered.length - 1) { index--; } else if (!movingUp && index === 0) { index++; } - const prev = ordered[movingUp ? index : index - 1]; const next = ordered[movingUp ? index + 1 : index]; let newOrder = _utils.DEFAULT_ALPHABET[0]; let ensureBeforeIsSane = false; - if (!prev) { // Move to front if (next?.order) { @@ -415,7 +360,6 @@ // Move somewhere in the middle const startOrder = prev?.order; const endOrder = next?.order; - if (startOrder && endOrder) { if (startOrder === endOrder) { // Error case: just move +1 to break out of awful math @@ -439,20 +383,16 @@ } } } - if (ensureBeforeIsSane) { // We were asked by the order algorithm to prepare the moving space for a landing // in the undefined order part of the order array, which means we need to update the // spaces that come before it with a stable order value. let lastOrder; - for (let i = 0; i <= index; i++) { const target = ordered[i]; - if (i === 0) { lastOrder = target.order; } - if (!target.order) { // XXX: We should be creating gaps to avoid conflicts lastOrder = lastOrder ? (0, _utils.nextString)(lastOrder) : _utils.DEFAULT_ALPHABET[0]; @@ -467,14 +407,14 @@ lastOrder = target.order; } } - if (lastOrder) { newOrder = (0, _utils.nextString)(lastOrder); } - } // TODO: Deal with order conflicts by reordering - // Now we can finally update our own order state + } + // TODO: Deal with order conflicts by reordering + // Now we can finally update our own order state const currentChild = parentRoom.currentState.getStateEvents(_event.EventType.SpaceChild, this.roomId); const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] @@ -484,23 +424,21 @@ order: newOrder }), this.roomId); } + /** * Creates (uploads) a new file to this tree. The file must have already been encrypted for the room. * The file contents are in a type that is compatible with MatrixClient.uploadContent(). - * @param {string} name The name of the file. - * @param {File | String | Buffer | ReadStream | Blob} encryptedContents The encrypted contents. - * @param {Partial} info The encrypted file information. - * @param {IContent} additionalContent Optional event content fields to include in the message. - * @returns {Promise} Resolves to the file event's sent response. + * @param name - The name of the file. + * @param encryptedContents - The encrypted contents. + * @param info - The encrypted file information. + * @param additionalContent - Optional event content fields to include in the message. + * @returns Promise which resolves to the file event's sent response. */ - - async createFile(name, encryptedContents, info, additionalContent) { - const mxc = await this.client.uploadContent(encryptedContents, { - includeFilename: false, - onlyContentUri: true, - rawResponse: false // make this explicit otherwise behaviour is different on browser vs NodeJS - + const { + content_uri: mxc + } = await this.client.uploadContent(encryptedContents, { + includeFilename: false }); info.url = mxc; const fileContent = { @@ -510,54 +448,47 @@ file: info }; additionalContent = additionalContent ?? {}; - if (additionalContent["m.new_content"]) { // We do the right thing according to the spec, but due to how relations are // handled we also end up duplicating this information to the regular `content` // as well. additionalContent["m.new_content"] = fileContent; } - const res = await this.client.sendMessage(this.roomId, _objectSpread(_objectSpread(_objectSpread({}, additionalContent), fileContent), {}, { [_event.UNSTABLE_MSC3089_LEAF.name]: {} })); await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, { active: true, name: name - }, res['event_id']); + }, res["event_id"]); return res; } + /** * Retrieves a file from the tree. - * @param {string} fileEventId The event ID of the file. - * @returns {MSC3089Branch | null} The file, or null if not found. + * @param fileEventId - The event ID of the file. + * @returns The file, or null if not found. */ - - getFile(fileEventId) { const branch = this.room.currentState.getStateEvents(_event.UNSTABLE_MSC3089_BRANCH.name, fileEventId); return branch ? new _MSC3089Branch.MSC3089Branch(this.client, branch, this) : null; } + /** * Gets an array of all known files for the tree. - * @returns {MSC3089Branch[]} The known files. May be empty, but not null. + * @returns The known files. May be empty, but not null. */ - - listFiles() { return this.listAllFiles().filter(b => b.isActive); } + /** * Gets an array of all known files for the tree, including inactive/invalid ones. - * @returns {MSC3089Branch[]} The known files. May be empty, but not null. + * @returns The known files. May be empty, but not null. */ - - listAllFiles() { const branches = this.room.currentState.getStateEvents(_event.UNSTABLE_MSC3089_BRANCH.name) ?? []; return branches.map(e => new _MSC3089Branch.MSC3089Branch(this.client, e, this)); } - } - exports.MSC3089TreeSpace = MSC3089TreeSpace; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/poll.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/poll.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/poll.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/poll.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,211 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PollEvent = exports.Poll = void 0; +var _polls = require("../@types/polls"); +var _relations = require("./relations"); +var _typedEventEmitter = require("./typed-event-emitter"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +let PollEvent; +exports.PollEvent = PollEvent; +(function (PollEvent) { + PollEvent["New"] = "Poll.new"; + PollEvent["End"] = "Poll.end"; + PollEvent["Update"] = "Poll.update"; + PollEvent["Responses"] = "Poll.Responses"; + PollEvent["Destroy"] = "Poll.Destroy"; + PollEvent["UndecryptableRelations"] = "Poll.UndecryptableRelations"; +})(PollEvent || (exports.PollEvent = PollEvent = {})); +const filterResponseRelations = (relationEvents, pollEndTimestamp) => { + const responseEvents = relationEvents.filter(event => { + if (event.isDecryptionFailure()) { + return; + } + return _polls.M_POLL_RESPONSE.matches(event.getType()) && + // From MSC3381: + // "Votes sent on or before the end event's timestamp are valid votes" + event.getTs() <= pollEndTimestamp; + }); + return { + responseEvents + }; +}; +class Poll extends _typedEventEmitter.TypedEventEmitter { + /** + * Keep track of undecryptable relations + * As incomplete result sets affect poll results + */ + + constructor(rootEvent, matrixClient, room) { + super(); + this.rootEvent = rootEvent; + this.matrixClient = matrixClient; + this.room = room; + _defineProperty(this, "roomId", void 0); + _defineProperty(this, "pollEvent", void 0); + _defineProperty(this, "_isFetchingResponses", false); + _defineProperty(this, "relationsNextBatch", void 0); + _defineProperty(this, "responses", null); + _defineProperty(this, "endEvent", void 0); + _defineProperty(this, "undecryptableRelationEventIds", new Set()); + _defineProperty(this, "countUndecryptableEvents", events => { + const undecryptableEventIds = events.filter(event => event.isDecryptionFailure()).map(event => event.getId()); + const previousCount = this.undecryptableRelationsCount; + this.undecryptableRelationEventIds = new Set([...this.undecryptableRelationEventIds, ...undecryptableEventIds]); + if (this.undecryptableRelationsCount !== previousCount) { + this.emit(PollEvent.UndecryptableRelations, this.undecryptableRelationsCount); + } + }); + if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) { + throw new Error("Invalid poll start event."); + } + this.roomId = this.rootEvent.getRoomId(); + this.pollEvent = this.rootEvent.unstableExtensibleEvent; + } + get pollId() { + return this.rootEvent.getId(); + } + get endEventId() { + return this.endEvent?.getId(); + } + get isEnded() { + return !!this.endEvent; + } + get isFetchingResponses() { + return this._isFetchingResponses; + } + get undecryptableRelationsCount() { + return this.undecryptableRelationEventIds.size; + } + async getResponses() { + // if we have already fetched some responses + // just return them + if (this.responses) { + return this.responses; + } + + // if there is no fetching in progress + // start fetching + if (!this.isFetchingResponses) { + await this.fetchResponses(); + } + // return whatever responses we got from the first page + return this.responses; + } + + /** + * + * @param event - event with a relation to the rootEvent + * @returns void + */ + onNewRelation(event) { + if (_polls.M_POLL_END.matches(event.getType()) && this.validateEndEvent(event)) { + this.endEvent = event; + this.refilterResponsesOnEnd(); + this.emit(PollEvent.End); + } + + // wait for poll responses to be initialised + if (!this.responses) { + return; + } + const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; + const { + responseEvents + } = filterResponseRelations([event], pollEndTimestamp); + this.countUndecryptableEvents([event]); + if (responseEvents.length) { + responseEvents.forEach(event => { + this.responses.addEvent(event); + }); + this.emit(PollEvent.Responses, this.responses); + } + } + async fetchResponses() { + this._isFetchingResponses = true; + + // we want: + // - stable and unstable M_POLL_RESPONSE + // - stable and unstable M_POLL_END + // so make one api call and filter by event type client side + const allRelations = await this.matrixClient.relations(this.roomId, this.rootEvent.getId(), "m.reference", undefined, { + from: this.relationsNextBatch || undefined + }); + await Promise.all(allRelations.events.map(event => this.matrixClient.decryptEventIfNeeded(event))); + const responses = this.responses || new _relations.Relations("m.reference", _polls.M_POLL_RESPONSE.name, this.matrixClient, [_polls.M_POLL_RESPONSE.altName]); + const pollEndEvent = allRelations.events.find(event => _polls.M_POLL_END.matches(event.getType())); + if (this.validateEndEvent(pollEndEvent)) { + this.endEvent = pollEndEvent; + this.refilterResponsesOnEnd(); + this.emit(PollEvent.End); + } + const pollCloseTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; + const { + responseEvents + } = filterResponseRelations(allRelations.events, pollCloseTimestamp); + responseEvents.forEach(event => { + responses.addEvent(event); + }); + this.relationsNextBatch = allRelations.nextBatch ?? undefined; + this.responses = responses; + this.countUndecryptableEvents(allRelations.events); + + // while there are more pages of relations + // fetch them + if (this.relationsNextBatch) { + // don't await + // we want to return the first page as soon as possible + this.fetchResponses(); + } else { + // no more pages + this._isFetchingResponses = false; + } + + // emit after updating _isFetchingResponses state + this.emit(PollEvent.Responses, this.responses); + } + + /** + * Only responses made before the poll ended are valid + * Refilter after an end event is recieved + * To ensure responses are valid + */ + refilterResponsesOnEnd() { + if (!this.responses) { + return; + } + const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER; + this.responses.getRelations().forEach(event => { + if (event.getTs() > pollEndTimestamp) { + this.responses?.removeEvent(event); + } + }); + this.emit(PollEvent.Responses, this.responses); + } + validateEndEvent(endEvent) { + if (!endEvent) { + return false; + } + /** + * Repeated end events are ignored - + * only the first (valid) closure event by origin_server_ts is counted. + */ + if (this.endEvent && this.endEvent.getTs() < endEvent.getTs()) { + return false; + } + + /** + * MSC3381 + * If a m.poll.end event is received from someone other than the poll creator or user with permission to redact + * others' messages in the room, the event must be ignored by clients due to being invalid. + */ + const roomCurrentState = this.room.currentState; + const endEventSender = endEvent.getSender(); + return !!endEventSender && (endEventSender === this.rootEvent.getSender() || roomCurrentState.maySendRedactionForEvent(this.rootEvent, endEventSender)); + } +} +exports.Poll = Poll; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/read-receipt.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/read-receipt.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/read-receipt.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/read-receipt.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,246 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ReadReceipt = void 0; +exports.synthesizeReceipt = synthesizeReceipt; +var _read_receipts = require("../@types/read_receipts"); +var _typedEventEmitter = require("./typed-event-emitter"); +var utils = _interopRequireWildcard(require("../utils")); +var _event = require("./event"); +var _event2 = require("../@types/event"); +var _room = require("./room"); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function synthesizeReceipt(userId, event, receiptType) { + return new _event.MatrixEvent({ + content: { + [event.getId()]: { + [receiptType]: { + [userId]: { + ts: event.getTs(), + thread_id: event.threadRootId ?? _read_receipts.MAIN_ROOM_TIMELINE + } + } + } + }, + type: _event2.EventType.Receipt, + room_id: event.getRoomId() + }); +} +const ReceiptPairRealIndex = 0; +const ReceiptPairSyntheticIndex = 1; +class ReadReceipt extends _typedEventEmitter.TypedEventEmitter { + constructor(...args) { + super(...args); + _defineProperty(this, "receipts", new utils.MapWithDefault(() => new Map())); + _defineProperty(this, "receiptCacheByEventId", new Map()); + _defineProperty(this, "timeline", void 0); + } + /** + * Gets the latest receipt for a given user in the room + * @param userId - The id of the user for which we want the receipt + * @param ignoreSynthesized - Whether to ignore synthesized receipts or not + * @param receiptType - Optional. The type of the receipt we want to get + * @returns the latest receipts of the chosen type for the chosen user + */ + getReadReceiptForUserId(userId, ignoreSynthesized = false, receiptType = _read_receipts.ReceiptType.Read) { + const [realReceipt, syntheticReceipt] = this.receipts.get(receiptType)?.get(userId) ?? [null, null]; + if (ignoreSynthesized) { + return realReceipt; + } + return syntheticReceipt ?? realReceipt; + } + + /** + * Get the ID of the event that a given user has read up to, or null if we + * have received no read receipts from them. + * @param userId - The user ID to get read receipt event ID for + * @param ignoreSynthesized - If true, return only receipts that have been + * sent by the server, not implicit ones generated + * by the JS SDK. + * @returns ID of the latest event that the given user has read, or null. + */ + getEventReadUpTo(userId, ignoreSynthesized = false) { + // XXX: This is very very ugly and I hope I won't have to ever add a new + // receipt type here again. IMHO this should be done by the server in + // some more intelligent manner or the client should just use timestamps + + const timelineSet = this.getUnfilteredTimelineSet(); + const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, _read_receipts.ReceiptType.Read); + const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, _read_receipts.ReceiptType.ReadPrivate); + + // If we have both, compare them + let comparison; + if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) { + comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId); + } + + // If we didn't get a comparison try to compare the ts of the receipts + if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) { + comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts; + } + + // The public receipt is more likely to drift out of date so the private + // one has precedence + if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null; + + // If public read receipt is older, return the private one + return (comparison < 0 ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null; + } + addReceiptToStructure(eventId, receiptType, userId, receipt, synthetic) { + const receiptTypesMap = this.receipts.getOrCreate(receiptType); + let pair = receiptTypesMap.get(userId); + if (!pair) { + pair = [null, null]; + receiptTypesMap.set(userId, pair); + } + let existingReceipt = pair[ReceiptPairRealIndex]; + if (synthetic) { + existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; + } + if (existingReceipt) { + // we only want to add this receipt if we think it is later than the one we already have. + // This is managed server-side, but because we synthesize RRs locally we have to do it here too. + const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId); + if (ordering !== null && ordering >= 0) { + return; + } + } + const wrappedReceipt = { + eventId, + data: receipt + }; + const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt; + const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex]; + let ordering = null; + if (realReceipt && syntheticReceipt) { + ordering = this.getUnfilteredTimelineSet().compareEventOrdering(realReceipt.eventId, syntheticReceipt.eventId); + } + const preferSynthetic = ordering === null || ordering < 0; + + // we don't bother caching just real receipts by event ID as there's nothing that would read it. + // Take the current cached receipt before we overwrite the pair elements. + const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; + if (synthetic && preferSynthetic) { + pair[ReceiptPairSyntheticIndex] = wrappedReceipt; + } else if (!synthetic) { + pair[ReceiptPairRealIndex] = wrappedReceipt; + if (!preferSynthetic) { + pair[ReceiptPairSyntheticIndex] = null; + } + } + const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; + if (cachedReceipt === newCachedReceipt) return; + + // clean up any previous cache entry + if (cachedReceipt && this.receiptCacheByEventId.get(cachedReceipt.eventId)) { + const previousEventId = cachedReceipt.eventId; + // Remove the receipt we're about to clobber out of existence from the cache + this.receiptCacheByEventId.set(previousEventId, this.receiptCacheByEventId.get(previousEventId).filter(r => { + return r.type !== receiptType || r.userId !== userId; + })); + if (this.receiptCacheByEventId.get(previousEventId).length < 1) { + this.receiptCacheByEventId.delete(previousEventId); // clean up the cache keys + } + } + + // cache the new one + if (!this.receiptCacheByEventId.get(eventId)) { + this.receiptCacheByEventId.set(eventId, []); + } + this.receiptCacheByEventId.get(eventId).push({ + userId: userId, + type: receiptType, + data: receipt + }); + } + + /** + * Get a list of receipts for the given event. + * @param event - the event to get receipts for + * @returns A list of receipts with a userId, type and data keys or + * an empty list. + */ + getReceiptsForEvent(event) { + return this.receiptCacheByEventId.get(event.getId()) || []; + } + /** + * This issue should also be addressed on synapse's side and is tracked as part + * of https://github.com/matrix-org/synapse/issues/14837 + * + * Retrieves the read receipt for the logged in user and checks if it matches + * the last event in the room and whether that event originated from the logged + * in user. + * Under those conditions we can consider the context as read. This is useful + * because we never send read receipts against our own events + * @param userId - the logged in user + */ + fixupNotifications(userId) { + const receipt = this.getReadReceiptForUserId(userId, false); + const lastEvent = this.timeline[this.timeline.length - 1]; + if (lastEvent && receipt?.eventId === lastEvent.getId() && userId === lastEvent.getSender()) { + this.setUnread(_room.NotificationCountType.Total, 0); + this.setUnread(_room.NotificationCountType.Highlight, 0); + } + } + + /** + * Add a temporary local-echo receipt to the room to reflect in the + * client the fact that we've sent one. + * @param userId - The user ID if the receipt sender + * @param e - The event that is to be acknowledged + * @param receiptType - The type of receipt + */ + addLocalEchoReceipt(userId, e, receiptType) { + this.addReceipt(synthesizeReceipt(userId, e, receiptType), true); + } + + /** + * Get a list of user IDs who have read up to the given event. + * @param event - the event to get read receipts for. + * @returns A list of user IDs. + */ + getUsersReadUpTo(event) { + return this.getReceiptsForEvent(event).filter(function (receipt) { + return utils.isSupportedReceiptType(receipt.type); + }).map(function (receipt) { + return receipt.userId; + }); + } + + /** + * Determines if the given user has read a particular event ID with the known + * history of the room. This is not a definitive check as it relies only on + * what is available to the room at the time of execution. + * @param userId - The user ID to check the read state of. + * @param eventId - The event ID to check if the user read. + * @returns True if the user has read the event, false otherwise. + */ + hasUserReadEvent(userId, eventId) { + const readUpToId = this.getEventReadUpTo(userId, false); + if (readUpToId === eventId) return true; + if (this.timeline?.length && this.timeline[this.timeline.length - 1].getSender() && this.timeline[this.timeline.length - 1].getSender() === userId) { + // It doesn't matter where the event is in the timeline, the user has read + // it because they've sent the latest event. + return true; + } + for (let i = this.timeline?.length - 1; i >= 0; --i) { + const ev = this.timeline[i]; + + // If we encounter the target event first, the user hasn't read it + // however if we encounter the readUpToId first then the user has read + // it. These rules apply because we're iterating bottom-up. + if (ev.getId() === eventId) return false; + if (ev.getId() === readUpToId) return true; + } + + // We don't know if the user has read it, so assume not. + return false; + } +} +exports.ReadReceipt = ReadReceipt; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/related-relations.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/related-relations.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/related-relations.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/related-relations.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,9 @@ value: true }); exports.RelatedRelations = void 0; - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* Copyright 2022 The Matrix.org Foundation C.I.C. @@ -22,25 +22,20 @@ See the License for the specific language governing permissions and limitations under the License. */ + class RelatedRelations { constructor(relations) { _defineProperty(this, "relations", void 0); - this.relations = relations.filter(r => !!r); } - getRelations() { return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []); } - on(ev, fn) { this.relations.forEach(r => r.on(ev, fn)); } - off(ev, fn) { this.relations.forEach(r => r.off(ev, fn)); } - } - exports.RelatedRelations = RelatedRelations; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations-container.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations-container.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations-container.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations-container.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,92 +4,78 @@ value: true }); exports.RelationsContainer = void 0; - var _relations = require("./relations"); - var _event = require("./event"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } class RelationsContainer { // A tree of objects to access a set of related children for an event, as in: // this.relations.get(parentEventId).get(relationType).get(relationEventType) + constructor(client, room) { this.client = client; this.room = room; - _defineProperty(this, "relations", new Map()); } + /** * Get a collection of child events to a given event in this timeline set. * - * @param {String} eventId - * The ID of the event that you'd like to access child events for. + * @param eventId - The ID of the event that you'd like to access child events for. * For example, with annotations, this would be the ID of the event being annotated. - * @param {String} relationType - * The type of relationship involved, such as "m.annotation", "m.reference", "m.replace", etc. - * @param {String} eventType - * The relation event's type, such as "m.reaction", etc. - * @throws If eventId, relationType or eventType + * @param relationType - The type of relationship involved, such as "m.annotation", "m.reference", "m.replace", etc. + * @param eventType - The relation event's type, such as "m.reaction", etc. + * @throws If `eventId, relationType or eventType` * are not valid. * - * @returns {?Relations} + * @returns * A container for relation events or undefined if there are no relation events for * the relationType. */ - - getChildEventsForEvent(eventId, relationType, eventType) { return this.relations.get(eventId)?.get(relationType)?.get(eventType); } - getAllChildEventsForEvent(parentEventId) { const relationsForEvent = this.relations.get(parentEventId) ?? new Map(); const events = []; - for (const relationsRecord of relationsForEvent.values()) { for (const relations of relationsRecord.values()) { events.push(...relations.getRelations()); } } - return events; } + /** * Set an event as the target event if any Relations exist for it already. * Child events can point to other child events as their parent, so this method may be * called for events which are also logically child events. * - * @param {MatrixEvent} event The event to check as relation target. + * @param event - The event to check as relation target. */ - - aggregateParentEvent(event) { const relationsForEvent = this.relations.get(event.getId()); if (!relationsForEvent) return; - for (const relationsWithRelType of relationsForEvent.values()) { for (const relationsWithEventType of relationsWithRelType.values()) { relationsWithEventType.setTargetEvent(event); } } } + /** * Add relation events to the relevant relation collection. * - * @param {MatrixEvent} event The new child event to be aggregated. - * @param {EventTimelineSet} timelineSet The event timeline set within which to search for the related event if any. + * @param event - The new child event to be aggregated. + * @param timelineSet - The event timeline set within which to search for the related event if any. */ - - aggregateChildEvent(event, timelineSet) { if (event.isRedacted() || event.status === _event.EventStatus.CANCELLED) { return; } - const relation = event.getRelation(); if (!relation) return; - const onEventDecrypted = () => { if (event.isDecryptionFailure()) { // This could for example happen if the encryption keys are not yet available. @@ -97,51 +83,40 @@ event.once(_event.MatrixEventEvent.Decrypted, onEventDecrypted); return; } - this.aggregateChildEvent(event, timelineSet); - }; // If the event is currently encrypted, wait until it has been decrypted. - + }; + // If the event is currently encrypted, wait until it has been decrypted. if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { event.once(_event.MatrixEventEvent.Decrypted, onEventDecrypted); return; } - const { event_id: relatesToEventId, rel_type: relationType } = relation; const eventType = event.getType(); let relationsForEvent = this.relations.get(relatesToEventId); - if (!relationsForEvent) { relationsForEvent = new Map(); this.relations.set(relatesToEventId, relationsForEvent); } - let relationsWithRelType = relationsForEvent.get(relationType); - if (!relationsWithRelType) { relationsWithRelType = new Map(); relationsForEvent.set(relationType, relationsWithRelType); } - let relationsWithEventType = relationsWithRelType.get(eventType); - if (!relationsWithEventType) { relationsWithEventType = new _relations.Relations(relationType, eventType, this.client); relationsWithRelType.set(eventType, relationsWithEventType); const room = this.room ?? timelineSet?.room; const relatesToEvent = timelineSet?.findEventById(relatesToEventId) ?? room?.findEventById(relatesToEventId) ?? room?.getPendingEvent(relatesToEventId); - if (relatesToEvent) { relationsWithEventType.setTargetEvent(relatesToEvent); } } - relationsWithEventType.addEvent(event); } - } - exports.RelationsContainer = RelationsContainer; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,27 +4,22 @@ value: true }); exports.RelationsEvent = exports.Relations = void 0; - var _event = require("./event"); - var _logger = require("../logger"); - var _event2 = require("../@types/event"); - var _typedEventEmitter = require("./typed-event-emitter"); - var _room = require("./room"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } let RelationsEvent; exports.RelationsEvent = RelationsEvent; - (function (RelationsEvent) { RelationsEvent["Add"] = "Relations.add"; RelationsEvent["Remove"] = "Relations.remove"; RelationsEvent["Redaction"] = "Relations.redaction"; })(RelationsEvent || (exports.RelationsEvent = RelationsEvent = {})); +const matchesEventType = (eventType, targetEventType, altTargetEventTypes = []) => [targetEventType, ...altTargetEventTypes].includes(eventType); /** * A container for relation events that supports easy access to common ways of @@ -36,58 +31,42 @@ */ class Relations extends _typedEventEmitter.TypedEventEmitter { /** - * @param {RelationType} relationType - * The type of relation involved, such as "m.annotation", "m.reference", - * "m.replace", etc. - * @param {String} eventType - * The relation event's type, such as "m.reaction", etc. - * @param {MatrixClient|Room} client - * The client which created this instance. For backwards compatibility also accepts a Room. + * @param relationType - The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc. + * @param eventType - The relation event's type, such as "m.reaction", etc. + * @param client - The client which created this instance. For backwards compatibility also accepts a Room. + * @param altEventTypes - alt event types for relation events, for example to support unstable prefixed event types */ - constructor(relationType, eventType, client) { + constructor(relationType, eventType, client, altEventTypes) { super(); this.relationType = relationType; this.eventType = eventType; - + this.altEventTypes = altEventTypes; _defineProperty(this, "relationEventIds", new Set()); - _defineProperty(this, "relations", new Set()); - _defineProperty(this, "annotationsByKey", {}); - _defineProperty(this, "annotationsBySender", {}); - _defineProperty(this, "sortedAnnotationsByKey", []); - _defineProperty(this, "targetEvent", null); - _defineProperty(this, "creationEmitted", false); - _defineProperty(this, "client", void 0); - _defineProperty(this, "onEventStatus", (event, status) => { if (!event.isSending()) { // Sending is done, so we don't need to listen anymore event.removeListener(_event.MatrixEventEvent.Status, this.onEventStatus); return; } - if (status !== _event.EventStatus.CANCELLED) { return; - } // Event was cancelled, remove from the collection - - + } + // Event was cancelled, remove from the collection event.removeListener(_event.MatrixEventEvent.Status, this.onEventStatus); this.removeEvent(event); }); - _defineProperty(this, "onBeforeRedaction", async redactedEvent => { if (!this.relations.has(redactedEvent)) { return; } - this.relations.delete(redactedEvent); - if (this.relationType === _event2.RelationType.Annotation) { // Remove the redacted annotation from aggregation by key this.removeAnnotationFromAggregation(redactedEvent); @@ -95,112 +74,77 @@ const lastReplacement = await this.getLastReplacement(); this.targetEvent.makeReplaced(lastReplacement); } - redactedEvent.removeListener(_event.MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); this.emit(RelationsEvent.Redaction, redactedEvent); }); - this.client = client instanceof _room.Room ? client.client : client; } + /** * Add relation events to this collection. * - * @param {MatrixEvent} event - * The new relation event to be added. + * @param event - The new relation event to be added. */ - - async addEvent(event) { if (this.relationEventIds.has(event.getId())) { return; } - const relation = event.getRelation(); - if (!relation) { _logger.logger.error("Event must have relation info"); - return; } - const relationType = relation.rel_type; const eventType = event.getType(); - - if (this.relationType !== relationType || this.eventType !== eventType) { + if (this.relationType !== relationType || !matchesEventType(eventType, this.eventType, this.altEventTypes)) { _logger.logger.error("Event relation info doesn't match this container"); - return; - } // If the event is in the process of being sent, listen for cancellation - // so we can remove the event from the collection. - + } + // If the event is in the process of being sent, listen for cancellation + // so we can remove the event from the collection. if (event.isSending()) { event.on(_event.MatrixEventEvent.Status, this.onEventStatus); } - this.relations.add(event); this.relationEventIds.add(event.getId()); - if (this.relationType === _event2.RelationType.Annotation) { this.addAnnotationToAggregation(event); } else if (this.relationType === _event2.RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) { const lastReplacement = await this.getLastReplacement(); this.targetEvent.makeReplaced(lastReplacement); } - event.on(_event.MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); this.emit(RelationsEvent.Add, event); this.maybeEmitCreated(); } + /** * Remove relation event from this collection. * - * @param {MatrixEvent} event - * The relation event to remove. + * @param event - The relation event to remove. */ - - async removeEvent(event) { if (!this.relations.has(event)) { return; } - - const relation = event.getRelation(); - - if (!relation) { - _logger.logger.error("Event must have relation info"); - - return; - } - - const relationType = relation.rel_type; - const eventType = event.getType(); - - if (this.relationType !== relationType || this.eventType !== eventType) { - _logger.logger.error("Event relation info doesn't match this container"); - - return; - } - this.relations.delete(event); - if (this.relationType === _event2.RelationType.Annotation) { this.removeAnnotationFromAggregation(event); } else if (this.relationType === _event2.RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) { const lastReplacement = await this.getLastReplacement(); this.targetEvent.makeReplaced(lastReplacement); } - this.emit(RelationsEvent.Remove, event); } + /** * Listens for event status changes to remove cancelled events. * - * @param {MatrixEvent} event The event whose status has changed - * @param {EventStatus} status The new status + * @param event - The event whose status has changed + * @param status - The new status */ - /** * Get all relation events in this collection. * @@ -208,32 +152,24 @@ * won't match timeline order in the case of scrollback. * TODO: Tweak `addEvent` to insert correctly for scrollback. * - * @return {Array} * Relation events in insertion order. */ getRelations() { return [...this.relations]; } - addAnnotationToAggregation(event) { const { key - } = event.getRelation(); - - if (!key) { - return; - } - + } = event.getRelation() ?? {}; + if (!key) return; let eventsForKey = this.annotationsByKey[key]; - if (!eventsForKey) { eventsForKey = this.annotationsByKey[key] = new Set(); this.sortedAnnotationsByKey.push([key, eventsForKey]); - } // Add the new event to the set for this key - - - eventsForKey.add(event); // Re-sort the [key, events] pairs in descending order of event count - + } + // Add the new event to the set for this key + eventsForKey.add(event); + // Re-sort the [key, events] pairs in descending order of event count this.sortedAnnotationsByKey.sort((a, b) => { const aEvents = a[1]; const bEvents = b[1]; @@ -241,43 +177,35 @@ }); const sender = event.getSender(); let eventsFromSender = this.annotationsBySender[sender]; - if (!eventsFromSender) { eventsFromSender = this.annotationsBySender[sender] = new Set(); - } // Add the new event to the set for this sender - - + } + // Add the new event to the set for this sender eventsFromSender.add(event); } - removeAnnotationFromAggregation(event) { const { key - } = event.getRelation(); - - if (!key) { - return; - } - + } = event.getRelation() ?? {}; + if (!key) return; const eventsForKey = this.annotationsByKey[key]; - if (eventsForKey) { - eventsForKey.delete(event); // Re-sort the [key, events] pairs in descending order of event count + eventsForKey.delete(event); + // Re-sort the [key, events] pairs in descending order of event count this.sortedAnnotationsByKey.sort((a, b) => { const aEvents = a[1]; const bEvents = b[1]; return bEvents.size - aEvents.size; }); } - const sender = event.getSender(); const eventsFromSender = this.annotationsBySender[sender]; - if (eventsFromSender) { eventsFromSender.delete(event); } } + /** * For relations that have been redacted, we want to remove them from * aggregation data sets and emit an update event. @@ -286,18 +214,15 @@ * - after the server accepted the redaction and remote echoed back to us * - before the original event has been marked redacted in the client * - * @param {MatrixEvent} redactedEvent - * The original relation event that is about to be redacted. + * @param redactedEvent - The original relation event that is about to be redacted. */ - /** * Get all events in this collection grouped by key and sorted by descending * event count in each group. * * This is currently only supported for the annotation relation type. * - * @return {Array} * An array of [key, events] pairs sorted by descending event count. * The events are stored in a Set (which preserves insertion order). */ @@ -306,118 +231,96 @@ // Other relation types are not grouped currently. return null; } - return this.sortedAnnotationsByKey; } + /** * Get all events in this collection grouped by sender. * * This is currently only supported for the annotation relation type. * - * @return {Object} * An object with each relation sender as a key and the matching Set of * events for that sender as a value. */ - - getAnnotationsBySender() { if (this.relationType !== _event2.RelationType.Annotation) { // Other relation types are not grouped currently. return null; } - return this.annotationsBySender; } + /** * Returns the most recent (and allowed) m.replace relation, if any. * * This is currently only supported for the m.replace relation type, * once the target event is known, see `addEvent`. - * - * @return {MatrixEvent?} */ - - async getLastReplacement() { if (this.relationType !== _event2.RelationType.Replace) { // Aggregating on last only makes sense for this relation type return null; } - if (!this.targetEvent) { // Don't know which replacements to accept yet. // This method shouldn't be called before the original // event is known anyway. return null; - } // the all-knowning server tells us that the event at some point had - // this timestamp for its replacement, so any following replacement should definitely not be less - + } + // the all-knowning server tells us that the event at some point had + // this timestamp for its replacement, so any following replacement should definitely not be less const replaceRelation = this.targetEvent.getServerAggregatedRelation(_event2.RelationType.Replace); const minTs = replaceRelation?.origin_server_ts; const lastReplacement = this.getRelations().reduce((last, event) => { if (event.getSender() !== this.targetEvent.getSender()) { return last; } - if (minTs && minTs > event.getTs()) { return last; } - if (last && last.getTs() > event.getTs()) { return last; } - return event; }, null); - - if (lastReplacement?.shouldAttemptDecryption()) { + if (lastReplacement?.shouldAttemptDecryption() && this.client.isCryptoEnabled()) { await lastReplacement.attemptDecryption(this.client.crypto); } else if (lastReplacement?.isBeingDecrypted()) { await lastReplacement.getDecryptionPromise(); } - return lastReplacement; } + /* - * @param {MatrixEvent} targetEvent the event the relations are related to. + * @param targetEvent - the event the relations are related to. */ - - async setTargetEvent(event) { if (this.targetEvent) { return; } - this.targetEvent = event; - if (this.relationType === _event2.RelationType.Replace && !this.targetEvent.isState()) { - const replacement = await this.getLastReplacement(); // this is the initial update, so only call it if we already have something + const replacement = await this.getLastReplacement(); + // this is the initial update, so only call it if we already have something // to not emit Event.replaced needlessly - if (replacement) { this.targetEvent.makeReplaced(replacement); } } - this.maybeEmitCreated(); } - maybeEmitCreated() { if (this.creationEmitted) { return; - } // Only emit we're "created" once we have a target event instance _and_ + } + // Only emit we're "created" once we have a target event instance _and_ // at least one related event. - - if (!this.targetEvent || !this.relations.size) { return; } - this.creationEmitted = true; this.targetEvent.emit(_event.MatrixEventEvent.RelationsCreated, this.relationType, this.eventType); } - } - exports.Relations = Relations; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/room.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/room.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/room.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/room.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,85 +4,43 @@ value: true }); exports.RoomNameType = exports.RoomEvent = exports.Room = exports.NotificationCountType = exports.KNOWN_SAFE_ROOM_VERSION = void 0; - +var _matrixEventsSdk = require("matrix-events-sdk"); var _eventTimelineSet = require("./event-timeline-set"); - var _eventTimeline = require("./event-timeline"); - var _contentRepo = require("../content-repo"); - var utils = _interopRequireWildcard(require("../utils")); - var _event = require("./event"); - var _eventStatus = require("./event-status"); - var _roomMember = require("./room-member"); - var _roomSummary = require("./room-summary"); - var _logger = require("../logger"); - var _ReEmitter = require("../ReEmitter"); - var _event2 = require("../@types/event"); - var _client = require("../client"); - var _filter = require("../filter"); - var _roomState = require("./room-state"); - var _beacon = require("./beacon"); - var _thread = require("./thread"); - -var _typedEventEmitter = require("./typed-event-emitter"); - var _read_receipts = require("../@types/read_receipts"); - var _relationsContainer = require("./relations-container"); - +var _readReceipt = require("./read-receipt"); +var _poll = require("./poll"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // These constants are used as sane defaults when the homeserver doesn't support // the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be // the same as the common default room version whereas SAFE_ROOM_VERSIONS are the // room versions which are considered okay for people to run without being asked // to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers // return an m.room_versions capability. -const KNOWN_SAFE_ROOM_VERSION = '9'; +const KNOWN_SAFE_ROOM_VERSION = "9"; exports.KNOWN_SAFE_ROOM_VERSION = KNOWN_SAFE_ROOM_VERSION; -const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; - -function synthesizeReceipt(userId, event, receiptType) { - // console.log("synthesizing receipt for "+event.getId()); - return new _event.MatrixEvent({ - content: { - [event.getId()]: { - [receiptType]: { - [userId]: { - ts: event.getTs() - } - } - } - }, - type: _event2.EventType.Receipt, - room_id: event.getRoomId() - }); -} - -const ReceiptPairRealIndex = 0; -const ReceiptPairSyntheticIndex = 1; // We will only hold a synthetic receipt if we do not have a real receipt or the synthetic is newer. - +const SAFE_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; // When inserting a visibility event affecting event `eventId`, we // need to scan through existing visibility events for `eventId`. // In theory, this could take an unlimited amount of time if: @@ -100,15 +58,12 @@ const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30; let NotificationCountType; exports.NotificationCountType = NotificationCountType; - (function (NotificationCountType) { NotificationCountType["Highlight"] = "highlight"; NotificationCountType["Total"] = "total"; })(NotificationCountType || (exports.NotificationCountType = NotificationCountType = {})); - let RoomEvent; exports.RoomEvent = RoomEvent; - (function (RoomEvent) { RoomEvent["MyMembership"] = "Room.myMembership"; RoomEvent["Tags"] = "Room.tags"; @@ -124,22 +79,26 @@ RoomEvent["OldStateUpdated"] = "Room.OldStateUpdated"; RoomEvent["CurrentStateUpdated"] = "Room.CurrentStateUpdated"; RoomEvent["HistoryImportedWithinTimeline"] = "Room.historyImportedWithinTimeline"; + RoomEvent["UnreadNotifications"] = "Room.UnreadNotifications"; })(RoomEvent || (exports.RoomEvent = RoomEvent = {})); - -class Room extends _typedEventEmitter.TypedEventEmitter { +class Room extends _readReceipt.ReadReceipt { // Pending in-flight requests { string: MatrixEvent } - // receipts should clobber based on receipt_type and user_id pairs hence - // the form of this structure. This is sub-optimal for the exposed APIs - // which pass in an event ID and get back some receipts, so we also store - // a pre-cached list for this purpose. - // { receipt_type: { user_id: IReceipt } } - // { event_id: ICachedReceipt[] } + + // Useful to know at what point the current user has started using threads in this room + + /** + * A record of the latest unthread receipts per user + * This is useful in determining whether a user has read a thread or not + */ + // any filtered timeline sets we're maintaining for this room // filter_id: timelineSet + // read by megolm via getter; boolean value - null indicates "use global value" + // flags to stop logspam about missing m.room.create events - // XXX: These should be read-only + // XXX: These should be read-only /** * The human-readable display name for this room. */ @@ -150,25 +109,19 @@ /** * Dict of room tags; the keys are the tag name and the values - * are any metadata associated with the tag - e.g. { "fav" : { order: 1 } } + * are any metadata associated with the tag - e.g. `{ "fav" : { order: 1 } }` */ // $tagName: { $metadata: $value } - /** * accountData Dict of per-room account_data events; the keys are the * event type and the values are the events. */ // $eventType: $event - /** * The room summary. */ - /** - * A token which a data store can use to remember the state of the room. - */ // legacy fields - /** * The live event timeline for this room, with the oldest event at index 0. * Present for backwards compatibility - prefer getLiveTimeline().getEvents() @@ -187,7 +140,8 @@ */ /** - * @experimental + * A collection of events known by the client + * This is not a comprehensive list of the threads that exist in this room */ /** @@ -225,183 +179,171 @@ *

In order that we can find events from their ids later, we also maintain a * map from event_id to timeline and index. * - * @constructor - * @alias module:models/room - * @param {string} roomId Required. The ID of this room. - * @param {MatrixClient} client Required. The client, used to lazy load members. - * @param {string} myUserId Required. The ID of the syncing user. - * @param {Object=} opts Configuration options - * @param {*} opts.storageToken Optional. The token which a data store can use - * to remember the state of the room. What this means is dependent on the store - * implementation. - * - * @param {String=} opts.pendingEventOrdering Controls where pending messages - * appear in a room's timeline. If "chronological", messages will appear - * in the timeline when the call to sendEvent was made. If - * "detached", pending messages will appear in a separate list, - * accessible via {@link module:models/room#getPendingEvents}. Default: - * "chronological". - * @param {boolean} [opts.timelineSupport = false] Set to true to enable improved - * timeline support. + * @param roomId - Required. The ID of this room. + * @param client - Required. The client, used to lazy load members. + * @param myUserId - Required. The ID of the syncing user. + * @param opts - Configuration options */ constructor(roomId, client, myUserId, opts = {}) { - super(); // In some cases, we add listeners for every displayed Matrix event, so it's + super(); + // In some cases, we add listeners for every displayed Matrix event, so it's // common to have quite a few more than the default limit. - this.roomId = roomId; this.client = client; this.myUserId = myUserId; this.opts = opts; - _defineProperty(this, "reEmitter", void 0); - - _defineProperty(this, "txnToEvent", {}); - - _defineProperty(this, "receipts", {}); - - _defineProperty(this, "receiptCacheByEventId", {}); - + _defineProperty(this, "txnToEvent", new Map()); _defineProperty(this, "notificationCounts", {}); - + _defineProperty(this, "threadNotifications", new Map()); + _defineProperty(this, "cachedThreadReadReceipts", new Map()); + _defineProperty(this, "oldestThreadedReceiptTs", Infinity); + _defineProperty(this, "unthreadedReceipts", new Map()); _defineProperty(this, "timelineSets", void 0); - + _defineProperty(this, "polls", new Map()); _defineProperty(this, "threadsTimelineSets", []); - _defineProperty(this, "filteredTimelineSets", {}); - _defineProperty(this, "timelineNeedsRefresh", false); - _defineProperty(this, "pendingEventList", void 0); - - _defineProperty(this, "blacklistUnverifiedDevices", null); - - _defineProperty(this, "selfMembership", null); - + _defineProperty(this, "blacklistUnverifiedDevices", void 0); + _defineProperty(this, "selfMembership", void 0); _defineProperty(this, "summaryHeroes", null); - _defineProperty(this, "getTypeWarning", false); - _defineProperty(this, "getVersionWarning", false); - _defineProperty(this, "membersPromise", void 0); - _defineProperty(this, "name", void 0); - _defineProperty(this, "normalizedName", void 0); - _defineProperty(this, "tags", {}); - - _defineProperty(this, "accountData", {}); - + _defineProperty(this, "accountData", new Map()); _defineProperty(this, "summary", null); - - _defineProperty(this, "storageToken", void 0); - _defineProperty(this, "timeline", void 0); - _defineProperty(this, "oldState", void 0); - _defineProperty(this, "currentState", void 0); - _defineProperty(this, "relations", new _relationsContainer.RelationsContainer(this.client, this)); - _defineProperty(this, "threads", new Map()); - _defineProperty(this, "lastThread", void 0); - _defineProperty(this, "visibilityEvents", new Map()); - _defineProperty(this, "threadTimelineSetsPromise", null); - _defineProperty(this, "threadsReady", false); - + _defineProperty(this, "updateThreadRootEvents", (thread, toStartOfTimeline, recreateEvent) => { + if (thread.length) { + this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline, recreateEvent); + if (thread.hasCurrentUserParticipated) { + this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline, recreateEvent); + } + } + }); + _defineProperty(this, "updateThreadRootEvent", (timelineSet, thread, toStartOfTimeline, recreateEvent) => { + if (timelineSet && thread.rootEvent) { + if (recreateEvent) { + timelineSet.removeEvent(thread.id); + } + if (_thread.Thread.hasServerSideSupport) { + timelineSet.addLiveEvent(thread.rootEvent, { + duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Replace, + fromCache: false, + roomState: this.currentState + }); + } else { + timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), { + toStartOfTimeline + }); + } + } + }); _defineProperty(this, "applyRedaction", event => { if (event.isRedaction()) { - const redactId = event.event.redacts; // if we know about this event, redact its contents now. - - const redactedEvent = this.findEventById(redactId); + const redactId = event.event.redacts; + // if we know about this event, redact its contents now. + const redactedEvent = redactId ? this.findEventById(redactId) : undefined; if (redactedEvent) { - redactedEvent.makeRedacted(event); // If this is in the current state, replace it with the redacted version + redactedEvent.makeRedacted(event); + // If this is in the current state, replace it with the redacted version if (redactedEvent.isState()) { const currentStateEvent = this.currentState.getStateEvents(redactedEvent.getType(), redactedEvent.getStateKey()); - - if (currentStateEvent.getId() === redactedEvent.getId()) { + if (currentStateEvent?.getId() === redactedEvent.getId()) { this.currentState.setStateEvents([redactedEvent]); } } + this.emit(RoomEvent.Redaction, event, this); - this.emit(RoomEvent.Redaction, event, this); // TODO: we stash user displaynames (among other things) in + // TODO: we stash user displaynames (among other things) in // RoomMember objects which are then attached to other events // (in the sender and target fields). We should get those // RoomMember objects to update themselves when the events that // they are based on are changed. + // Remove any visibility change on this event. + this.visibilityEvents.delete(redactId); - this.visibilityEvents.delete(redactId); // If this event is a visibility change event, remove it from the + // If this event is a visibility change event, remove it from the // list of visibility changes and update any event affected by it. - if (redactedEvent.isVisibilityEvent()) { this.redactVisibilityChangeEvent(event); } - } // FIXME: apply redactions to notification list + } + + // FIXME: apply redactions to notification list + // NB: We continue to add the redaction event to the timeline so // clients can say "so and so redacted an event" if they wish to. Also // this may be needed to trigger an update. - } }); - this.setMaxListeners(100); this.reEmitter = new _ReEmitter.TypedReEmitter(this); opts.pendingEventOrdering = opts.pendingEventOrdering || _client.PendingEventOrdering.Chronological; - this.name = roomId; // all our per-room timeline sets. the first one is the unfiltered ones; - // the subsequent ones are the filtered ones in no particular order. + this.name = roomId; + this.normalizedName = roomId; + // all our per-room timeline sets. the first one is the unfiltered ones; + // the subsequent ones are the filtered ones in no particular order. this.timelineSets = [new _eventTimelineSet.EventTimelineSet(this, opts)]; this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [RoomEvent.Timeline, RoomEvent.TimelineReset]); this.fixUpLegacyTimelineFields(); - if (this.opts.pendingEventOrdering === _client.PendingEventOrdering.Detached) { this.pendingEventList = []; this.client.store.getPendingEvents(this.roomId).then(events => { + const mapper = this.client.getEventMapper({ + toDevice: false, + decrypt: false + }); events.forEach(async serializedEvent => { - const event = new _event.MatrixEvent(serializedEvent); - - if (event.getType() === _event2.EventType.RoomMessageEncrypted) { - await event.attemptDecryption(this.client.crypto); - } - + const event = mapper(serializedEvent); + await client.decryptEventIfNeeded(event); event.setStatus(_eventStatus.EventStatus.NOT_SENT); this.addPendingEvent(event, event.getTxnId()); }); }); - } // awaited by getEncryptionTargetMembers while room members are loading - + } + // awaited by getEncryptionTargetMembers while room members are loading if (!this.opts.lazyLoadMembers) { this.membersPromise = Promise.resolve(false); } else { - this.membersPromise = null; + this.membersPromise = undefined; } } - async createThreadsTimelineSets() { if (this.threadTimelineSetsPromise) { return this.threadTimelineSetsPromise; } - - if (this.client?.supportsExperimentalThreads()) { + if (this.client?.supportsThreads()) { try { this.threadTimelineSetsPromise = Promise.all([this.createThreadTimelineSet(), this.createThreadTimelineSet(_thread.ThreadFilterType.My)]); const timelineSets = await this.threadTimelineSetsPromise; this.threadsTimelineSets.push(...timelineSets); + return timelineSets; } catch (e) { this.threadTimelineSetsPromise = null; + return null; } } + return null; } + /** * Bulk decrypt critical events in a room * @@ -411,75 +353,66 @@ * - Last event of every room (to generate likely message preview) * - All events up to the read receipt (to calculate an accurate notification count) * - * @returns {Promise} Signals when all events have been decrypted + * @returns Signals when all events have been decrypted */ - - - decryptCriticalEvents() { + async decryptCriticalEvents() { + if (!this.client.isCryptoEnabled()) return; const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId(), true); const events = this.getLiveTimeline().getEvents(); const readReceiptTimelineIndex = events.findIndex(matrixEvent => { return matrixEvent.event.event_id === readReceiptEventId; }); - const decryptionPromises = events.slice(readReceiptTimelineIndex).filter(event => event.shouldAttemptDecryption()).reverse().map(event => event.attemptDecryption(this.client.crypto, { + const decryptionPromises = events.slice(readReceiptTimelineIndex).reverse().map(event => this.client.decryptEventIfNeeded(event, { isRetry: true })); - return Promise.allSettled(decryptionPromises); + await Promise.allSettled(decryptionPromises); } + /** * Bulk decrypt events in a room * - * @returns {Promise} Signals when all events have been decrypted + * @returns Signals when all events have been decrypted */ - - - decryptAllEvents() { - const decryptionPromises = this.getUnfilteredTimelineSet().getLiveTimeline().getEvents().filter(event => event.shouldAttemptDecryption()).reverse().map(event => event.attemptDecryption(this.client.crypto, { + async decryptAllEvents() { + if (!this.client.isCryptoEnabled()) return; + const decryptionPromises = this.getUnfilteredTimelineSet().getLiveTimeline().getEvents().slice(0) // copy before reversing + .reverse().map(event => this.client.decryptEventIfNeeded(event, { isRetry: true })); - return Promise.allSettled(decryptionPromises); + await Promise.allSettled(decryptionPromises); } + /** * Gets the creator of the room - * @returns {string} The creator of the room, or null if it could not be determined + * @returns The creator of the room, or null if it could not be determined */ - - getCreator() { const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); - return createEvent?.getContent()['creator'] ?? null; + return createEvent?.getContent()["creator"] ?? null; } + /** * Gets the version of the room - * @returns {string} The version of the room, or null if it could not be determined + * @returns The version of the room, or null if it could not be determined */ - - getVersion() { const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); - if (!createEvent) { if (!this.getVersionWarning) { _logger.logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event"); - this.getVersionWarning = true; } - - return '1'; + return "1"; } - - const ver = createEvent.getContent()['room_version']; - if (ver === undefined) return '1'; - return ver; + return createEvent.getContent()["room_version"] ?? "1"; } + /** * Determines whether this room needs to be upgraded to a new version - * @returns {string?} What version the room should be upgraded to, or null if + * @returns What version the room should be upgraded to, or null if * the room does not require upgrading at this time. * @deprecated Use #getRecommendedVersion() instead */ - - shouldUpgradeToVersion() { // TODO: Remove this function. // This makes assumptions about which versions are safe, and can easily @@ -487,42 +420,37 @@ // which determines a safer value. This function doesn't use that function // because this is not async-capable, and to avoid breaking the contract // we're deprecating this. + if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) { return KNOWN_SAFE_ROOM_VERSION; } - return null; } + /** * Determines the recommended room version for the room. This returns an - * object with 3 properties: version as the new version the + * object with 3 properties: `version` as the new version the * room should be upgraded to (may be the same as the current version); - * needsUpgrade to indicate if the room actually can be - * upgraded (ie: does the current version not match?); and urgent + * `needsUpgrade` to indicate if the room actually can be + * upgraded (ie: does the current version not match?); and `urgent` * to indicate if the new version patches a vulnerability in a previous * version. - * @returns {Promise<{version: string, needsUpgrade: boolean, urgent: boolean}>} + * @returns * Resolves to the version the room should be upgraded to. */ - - async getRecommendedVersion() { const capabilities = await this.client.getCapabilities(); let versionCap = capabilities["m.room_versions"]; - if (!versionCap) { versionCap = { default: KNOWN_SAFE_ROOM_VERSION, available: {} }; - for (const safeVer of SAFE_ROOM_VERSIONS) { versionCap.available[safeVer] = _client.RoomVersionStability.Stable; } } - let result = this.checkVersionAgainstCapability(versionCap); - if (result.urgent && result.needsUpgrade) { // Something doesn't feel right: we shouldn't need to update // because the version we're on should be in the protocol's @@ -531,156 +459,127 @@ // room version is not stable. As a solution, we'll refresh // the capability we're using to determine this. _logger.logger.warn("Refreshing room version capability because the server looks " + "to be supporting a newer room version we don't know about."); - const caps = await this.client.getCapabilities(true); versionCap = caps["m.room_versions"]; - if (!versionCap) { _logger.logger.warn("No room version capability - assuming upgrade required."); - return result; } else { result = this.checkVersionAgainstCapability(versionCap); } } - return result; } - checkVersionAgainstCapability(versionCap) { const currentVersion = this.getVersion(); - _logger.logger.log(`[${this.roomId}] Current version: ${currentVersion}`); - _logger.logger.log(`[${this.roomId}] Version capability: `, versionCap); - const result = { version: currentVersion, needsUpgrade: false, urgent: false - }; // If the room is on the default version then nothing needs to change + }; + // If the room is on the default version then nothing needs to change if (currentVersion === versionCap.default) return result; - const stableVersions = Object.keys(versionCap.available).filter(v => versionCap.available[v] === 'stable'); // Check if the room is on an unstable version. We determine urgency based + const stableVersions = Object.keys(versionCap.available).filter(v => versionCap.available[v] === "stable"); + + // Check if the room is on an unstable version. We determine urgency based // off the version being in the Matrix spec namespace or not (if the version // is in the current namespace and unstable, the room is probably vulnerable). - if (!stableVersions.includes(currentVersion)) { result.version = versionCap.default; result.needsUpgrade = true; result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g); - if (result.urgent) { _logger.logger.warn(`URGENT upgrade required on ${this.roomId}`); } else { _logger.logger.warn(`Non-urgent upgrade required on ${this.roomId}`); } - return result; - } // The room is on a stable, but non-default, version by this point. - // No upgrade needed. - + } + // The room is on a stable, but non-default, version by this point. + // No upgrade needed. return result; } + /** * Determines whether the given user is permitted to perform a room upgrade - * @param {String} userId The ID of the user to test against - * @returns {boolean} True if the given user is permitted to upgrade the room + * @param userId - The ID of the user to test against + * @returns True if the given user is permitted to upgrade the room */ - - userMayUpgradeRoom(userId) { return this.currentState.maySendStateEvent(_event2.EventType.RoomTombstone, userId); } + /** * Get the list of pending sent events for this room * - * @return {module:models/event.MatrixEvent[]} A list of the sent events + * @returns A list of the sent events * waiting for remote echo. * - * @throws If opts.pendingEventOrdering was not 'detached' + * @throws If `opts.pendingEventOrdering` was not 'detached' */ - - getPendingEvents() { - if (this.opts.pendingEventOrdering !== _client.PendingEventOrdering.Detached) { + if (!this.pendingEventList) { throw new Error("Cannot call getPendingEvents with pendingEventOrdering == " + this.opts.pendingEventOrdering); } - return this.pendingEventList; } + /** * Removes a pending event for this room * - * @param {string} eventId - * @return {boolean} True if an element was removed. + * @returns True if an element was removed. */ - - removePendingEvent(eventId) { - if (this.opts.pendingEventOrdering !== _client.PendingEventOrdering.Detached) { + if (!this.pendingEventList) { throw new Error("Cannot call removePendingEvent with pendingEventOrdering == " + this.opts.pendingEventOrdering); } - const removed = utils.removeElement(this.pendingEventList, function (ev) { return ev.getId() == eventId; }, false); this.savePendingEvents(); return removed; } + /** * Check whether the pending event list contains a given event by ID. * If pending event ordering is not "detached" then this returns false. * - * @param {string} eventId The event ID to check for. - * @return {boolean} + * @param eventId - The event ID to check for. */ - - hasPendingEvent(eventId) { - if (this.opts.pendingEventOrdering !== _client.PendingEventOrdering.Detached) { - return false; - } - - return this.pendingEventList.some(event => event.getId() === eventId); + return this.pendingEventList?.some(event => event.getId() === eventId) ?? false; } + /** * Get a specific event from the pending event list, if configured, null otherwise. * - * @param {string} eventId The event ID to check for. - * @return {MatrixEvent} + * @param eventId - The event ID to check for. */ - - getPendingEvent(eventId) { - if (this.opts.pendingEventOrdering !== _client.PendingEventOrdering.Detached) { - return null; - } - - return this.pendingEventList.find(event => event.getId() === eventId); + return this.pendingEventList?.find(event => event.getId() === eventId) ?? null; } + /** * Get the live unfiltered timeline for this room. * - * @return {module:models/event-timeline~EventTimeline} live timeline + * @returns live timeline */ - - getLiveTimeline() { return this.getUnfilteredTimelineSet().getLiveTimeline(); } + /** * Get the timestamp of the last message in the room * - * @return {number} the timestamp of the last message in the room + * @returns the timestamp of the last message in the room */ - - getLastActiveTimestamp() { const timeline = this.getLiveTimeline(); const events = timeline.getEvents(); - if (events.length) { const lastEvent = events[events.length - 1]; return lastEvent.getTs(); @@ -688,116 +587,90 @@ return Number.MIN_SAFE_INTEGER; } } + /** - * @return {string} the membership type (join | leave | invite) for the logged in user + * @returns the membership type (join | leave | invite) for the logged in user */ - - getMyMembership() { - return this.selfMembership; + return this.selfMembership ?? "leave"; } + /** * If this room is a DM we're invited to, * try to find out who invited us - * @return {string} user id of the inviter + * @returns user id of the inviter */ - - getDMInviter() { - if (this.myUserId) { - const me = this.getMember(this.myUserId); - - if (me) { - return me.getDMInviter(); - } + const me = this.getMember(this.myUserId); + if (me) { + return me.getDMInviter(); } - if (this.selfMembership === "invite") { // fall back to summary information const memberCount = this.getInvitedAndJoinedMemberCount(); - - if (memberCount == 2 && this.summaryHeroes.length) { - return this.summaryHeroes[0]; + if (memberCount === 2) { + return this.summaryHeroes?.[0]; } } } + /** * Assuming this room is a DM room, tries to guess with which user. - * @return {string} user id of the other member (could be syncing user) + * @returns user id of the other member (could be syncing user) */ - - guessDMUserId() { const me = this.getMember(this.myUserId); - if (me) { const inviterId = me.getDMInviter(); - if (inviterId) { return inviterId; } - } // remember, we're assuming this room is a DM, - // so returning the first member we find should be fine - - - const hasHeroes = Array.isArray(this.summaryHeroes) && this.summaryHeroes.length; - - if (hasHeroes) { + } + // Remember, we're assuming this room is a DM, so returning the first member we find should be fine + if (Array.isArray(this.summaryHeroes) && this.summaryHeroes.length) { return this.summaryHeroes[0]; } - const members = this.currentState.getMembers(); const anyMember = members.find(m => m.userId !== this.myUserId); - if (anyMember) { return anyMember.userId; - } // it really seems like I'm the only user in the room + } + // it really seems like I'm the only user in the room // so I probably created a room with just me in it // and marked it as a DM. Ok then - - return this.myUserId; } - getAvatarFallbackMember() { const memberCount = this.getInvitedAndJoinedMemberCount(); - if (memberCount > 2) { return; } - const hasHeroes = Array.isArray(this.summaryHeroes) && this.summaryHeroes.length; - if (hasHeroes) { const availableMember = this.summaryHeroes.map(userId => { return this.getMember(userId); }).find(member => !!member); - if (availableMember) { return availableMember; } } - - const members = this.currentState.getMembers(); // could be different than memberCount + const members = this.currentState.getMembers(); + // could be different than memberCount // as this includes left members - if (members.length <= 2) { const availableMember = members.find(m => { return m.userId !== this.myUserId; }); - if (availableMember) { return availableMember; } - } // if all else fails, try falling back to a user, + } + // if all else fails, try falling back to a user, // and create a one-off member for it - - if (hasHeroes) { const availableUser = this.summaryHeroes.map(userId => { return this.client.getUser(userId); }).find(user => !!user); - if (availableUser) { const member = new _roomMember.RoomMember(this.roomId, availableUser.userId); member.user = availableUser; @@ -805,95 +678,94 @@ } } } + /** * Sets the membership this room was received as during sync - * @param {string} membership join | leave | invite + * @param membership - join | leave | invite */ - - updateMyMembership(membership) { const prevMembership = this.selfMembership; this.selfMembership = membership; - if (prevMembership !== membership) { if (membership === "leave") { this.cleanupAfterLeaving(); } - this.emit(RoomEvent.MyMembership, this, membership, prevMembership); } } - async loadMembersFromServer() { const lastSyncToken = this.client.store.getSyncToken(); - const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken); + const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken ?? undefined); return response.chunk; } - async loadMembers() { // were the members loaded from the server? let fromServer = false; - let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId); // If the room is encrypted, we always fetch members from the server at + let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId); + // If the room is encrypted, we always fetch members from the server at // least once, in case the latest state wasn't persisted properly. Note // that this function is only called once (unless loading the members // fails), since loadMembersIfNeeded always returns this.membersPromise // if set, which will be the result of the first (successful) call. - if (rawMembersEvents === null || this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId)) { fromServer = true; rawMembersEvents = await this.loadMembersFromServer(); - _logger.logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`); } - - const memberEvents = rawMembersEvents.map(this.client.getEventMapper()); + const memberEvents = rawMembersEvents.filter(utils.noUnsafeEventProps).map(this.client.getEventMapper()); return { memberEvents, fromServer }; } + + /** + * Check if loading of out-of-band-members has completed + * + * @returns true if the full membership list of this room has been loaded (including if lazy-loading is disabled). + * False if the load is not started or is in progress. + */ + membersLoaded() { + if (!this.opts.lazyLoadMembers) { + return true; + } + return this.currentState.outOfBandMembersReady(); + } + /** * Preloads the member list in case lazy loading * of memberships is in use. Can be called multiple times, * it will only preload once. - * @return {Promise} when preloading is done and + * @returns when preloading is done and * accessing the members on the room will take * all members in the room into account */ - - loadMembersIfNeeded() { if (this.membersPromise) { return this.membersPromise; - } // mark the state so that incoming messages while + } + + // mark the state so that incoming messages while // the request is in flight get marked as superseding // the OOB members - - this.currentState.markOutOfBandMembersStarted(); const inMemoryUpdate = this.loadMembers().then(result => { - this.currentState.setOutOfBandMembers(result.memberEvents); // now the members are loaded, start to track the e2e devices if needed - - if (this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId)) { - this.client.crypto.trackRoomDevices(this.roomId); - } - + this.currentState.setOutOfBandMembers(result.memberEvents); return result.fromServer; }).catch(err => { // allow retries on fail - this.membersPromise = null; + this.membersPromise = undefined; this.currentState.markOutOfBandMembersFailed(); throw err; - }); // update members in storage, but don't wait for it - + }); + // update members in storage, but don't wait for it inMemoryUpdate.then(fromServer => { if (fromServer) { - const oobMembers = this.currentState.getMembers().filter(m => m.isOutOfBand()).map(m => m.events.member.event); - + const oobMembers = this.currentState.getMembers().filter(m => m.isOutOfBand()).map(m => m.events.member?.event); _logger.logger.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`); - const store = this.client.store; - return store.setOutOfBandMembers(this.roomId, oobMembers) // swallow any IDB error as we don't want to fail + return store.setOutOfBandMembers(this.roomId, oobMembers) + // swallow any IDB error as we don't want to fail // because of this .catch(err => { _logger.logger.log("LL: storing OOB room members failed, oh well", err); @@ -907,35 +779,33 @@ this.membersPromise = inMemoryUpdate; return this.membersPromise; } + /** * Removes the lazily loaded members from storage if needed */ - - async clearLoadedMembersIfNeeded() { if (this.opts.lazyLoadMembers && this.membersPromise) { await this.loadMembersIfNeeded(); await this.client.store.clearOutOfBandMembers(this.roomId); this.currentState.clearOutOfBandMembers(); - this.membersPromise = null; + this.membersPromise = undefined; } } + /** * called when sync receives this room in the leave section * to do cleanup after leaving a room. Possibly called multiple times. */ - - cleanupAfterLeaving() { this.clearLoadedMembersIfNeeded().catch(err => { _logger.logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`); - _logger.logger.log(err); }); } + /** * Empty out the current live timeline and re-request it. This is used when - * historical messages are imported into the room via MSC2716 `/batch_send + * historical messages are imported into the room via MSC2716 `/batch_send` * because the client may already have that section of the timeline loaded. * We need to force the client to throw away their current timeline so that * when they back paginate over the area again with the historical messages @@ -946,26 +816,23 @@ * valid marker and can check the needs refresh status via * `room.getTimelineNeedsRefresh()`. */ - - async refreshLiveTimeline() { const liveTimelineBefore = this.getLiveTimeline(); const forwardPaginationToken = liveTimelineBefore.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS); const backwardPaginationToken = liveTimelineBefore.getPaginationToken(_eventTimeline.EventTimeline.BACKWARDS); const eventsBefore = liveTimelineBefore.getEvents(); const mostRecentEventInTimeline = eventsBefore[eventsBefore.length - 1]; + _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] at ` + `mostRecentEventInTimeline=${mostRecentEventInTimeline && mostRecentEventInTimeline.getId()} ` + `liveTimelineBefore=${liveTimelineBefore.toString()} ` + `forwardPaginationToken=${forwardPaginationToken} ` + `backwardPaginationToken=${backwardPaginationToken}`); - _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] at ` + `mostRecentEventInTimeline=${mostRecentEventInTimeline && mostRecentEventInTimeline.getId()} ` + `liveTimelineBefore=${liveTimelineBefore.toString()} ` + `forwardPaginationToken=${forwardPaginationToken} ` + `backwardPaginationToken=${backwardPaginationToken}`); // Get the main TimelineSet - - + // Get the main TimelineSet const timelineSet = this.getUnfilteredTimelineSet(); - let newTimeline; // If there isn't any event in the timeline, let's go fetch the latest + let newTimeline; + // If there isn't any event in the timeline, let's go fetch the latest // event and construct a timeline from it. // // This should only really happen if the user ran into an error // with refreshing the timeline before which left them in a blank // timeline from `resetLiveTimeline`. - if (!mostRecentEventInTimeline) { newTimeline = await this.client.getLatestTimeline(timelineSet); } else { @@ -976,103 +843,108 @@ // `timelineSet` empty so that the `client.getEventTimeline(...)` call // later, will call `/context` and create a new timeline instead of // returning the same one. - this.resetLiveTimeline(null, null); // Make the UI timeline show the new blank live timeline we just + this.resetLiveTimeline(null, null); + + // Make the UI timeline show the new blank live timeline we just // reset so that if the network fails below it's showing the // accurate state of what we're working with instead of the // disconnected one in the TimelineWindow which is just hanging // around by reference. + this.emit(RoomEvent.TimelineRefresh, this, timelineSet); - this.emit(RoomEvent.TimelineRefresh, this, timelineSet); // Use `client.getEventTimeline(...)` to construct a new timeline from a + // Use `client.getEventTimeline(...)` to construct a new timeline from a // `/context` response state and events for the most recent event before // we reset everything. The `timelineSet` we pass in needs to be empty // in order for this function to call `/context` and generate a new // timeline. - newTimeline = await this.client.getEventTimeline(timelineSet, mostRecentEventInTimeline.getId()); - } // If a racing `/sync` beat us to creating a new timeline, use that + } + + // If a racing `/sync` beat us to creating a new timeline, use that // instead because it's the latest in the room and any new messages in // the scrollback will include the history. - - const liveTimeline = timelineSet.getLiveTimeline(); - if (!liveTimeline || liveTimeline.getPaginationToken(_eventTimeline.Direction.Forward) === null && liveTimeline.getPaginationToken(_eventTimeline.Direction.Backward) === null && liveTimeline.getEvents().length === 0) { - _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] using our new live timeline`); // Set the pagination token back to the live sync token (`null`) instead + _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] using our new live timeline`); + // Set the pagination token back to the live sync token (`null`) instead // of using the `/context` historical token (ex. `t12-13_0_0_0_0_0_0_0_0`) // so that it matches the next response from `/sync` and we can properly // continue the timeline. + newTimeline.setPaginationToken(forwardPaginationToken, _eventTimeline.EventTimeline.FORWARDS); - - newTimeline.setPaginationToken(forwardPaginationToken, _eventTimeline.EventTimeline.FORWARDS); // Set our new fresh timeline as the live timeline to continue syncing + // Set our new fresh timeline as the live timeline to continue syncing // forwards and back paginating from. - - timelineSet.setLiveTimeline(newTimeline); // Fixup `this.oldstate` so that `scrollback` has the pagination tokens + timelineSet.setLiveTimeline(newTimeline); + // Fixup `this.oldstate` so that `scrollback` has the pagination tokens // available - this.fixUpLegacyTimelineFields(); } else { _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] \`/sync\` or some other request beat us to creating a new ` + `live timeline after we reset it. We'll use that instead since any events in the scrollback from ` + `this timeline will include the history.`); - } // The timeline has now been refreshed ✅ + } + // The timeline has now been refreshed ✅ + this.setTimelineNeedsRefresh(false); - this.setTimelineNeedsRefresh(false); // Emit an event which clients can react to and re-load the timeline + // Emit an event which clients can react to and re-load the timeline // from the SDK - this.emit(RoomEvent.TimelineRefresh, this, timelineSet); } + /** * Reset the live timeline of all timelineSets, and start new ones. * *

This is used when /sync returns a 'limited' timeline. * - * @param {string=} backPaginationToken token for back-paginating the new timeline - * @param {string=} forwardPaginationToken token for forward-paginating the old live timeline, + * @param backPaginationToken - token for back-paginating the new timeline + * @param forwardPaginationToken - token for forward-paginating the old live timeline, * if absent or null, all timelines are reset, removing old ones (including the previous live * timeline which would otherwise be unable to paginate forwards without this token). * Removing just the old live timeline whilst preserving previous ones is not supported. */ - - resetLiveTimeline(backPaginationToken, forwardPaginationToken) { - for (let i = 0; i < this.timelineSets.length; i++) { - this.timelineSets[i].resetLiveTimeline(backPaginationToken, forwardPaginationToken); + for (const timelineSet of this.timelineSets) { + timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined); + } + for (const thread of this.threads.values()) { + thread.resetLiveTimeline(backPaginationToken, forwardPaginationToken); } - this.fixUpLegacyTimelineFields(); } + /** * Fix up this.timeline, this.oldState and this.currentState * - * @private + * @internal */ - - fixUpLegacyTimelineFields() { const previousOldState = this.oldState; - const previousCurrentState = this.currentState; // maintain this.timeline as a reference to the live timeline, + const previousCurrentState = this.currentState; + + // maintain this.timeline as a reference to the live timeline, // and this.oldState and this.currentState as references to the // state at the start and end of that timeline. These are more // for backwards-compatibility than anything else. - this.timeline = this.getLiveTimeline().getEvents(); this.oldState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.BACKWARDS); - this.currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); // Let people know to register new listeners for the new state + this.currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); + + // Let people know to register new listeners for the new state // references. The reference won't necessarily change every time so only // emit when we see a change. - if (previousOldState !== this.oldState) { this.emit(RoomEvent.OldStateUpdated, this, previousOldState, this.oldState); } - if (previousCurrentState !== this.currentState) { - this.emit(RoomEvent.CurrentStateUpdated, this, previousCurrentState, this.currentState); // Re-emit various events on the current room state + this.emit(RoomEvent.CurrentStateUpdated, this, previousCurrentState, this.currentState); + + // Re-emit various events on the current room state // TODO: If currentState really only exists for backwards // compatibility, shouldn't we be doing this some other way? - this.reEmitter.stopReEmitting(previousCurrentState, [_roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update, _roomState.RoomStateEvent.Marker, _beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]); this.reEmitter.reEmit(this.currentState, [_roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update, _roomState.RoomStateEvent.Marker, _beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]); } } + /** * Returns whether there are any devices in the room that are unverified * @@ -1080,155 +952,229 @@ * disabled, then we aren't tracking room devices at all, so we can't answer this, and an * error will be thrown. * - * @return {boolean} the result + * @returns the result */ - - async hasUnverifiedDevices() { if (!this.client.isRoomEncrypted(this.roomId)) { return false; } - const e2eMembers = await this.getEncryptionTargetMembers(); - for (const member of e2eMembers) { const devices = this.client.getStoredDevicesForUser(member.userId); - if (devices.some(device => device.isUnverified())) { return true; } } - return false; } + /** * Return the timeline sets for this room. - * @return {EventTimelineSet[]} array of timeline sets for this room + * @returns array of timeline sets for this room */ - - getTimelineSets() { return this.timelineSets; } + /** * Helper to return the main unfiltered timeline set for this room - * @return {EventTimelineSet} room's unfiltered timeline set + * @returns room's unfiltered timeline set */ - - getUnfilteredTimelineSet() { return this.timelineSets[0]; } + /** * Get the timeline which contains the given event from the unfiltered set, if any * - * @param {string} eventId event ID to look for - * @return {?module:models/event-timeline~EventTimeline} timeline containing + * @param eventId - event ID to look for + * @returns timeline containing * the given event, or null if unknown */ - - getTimelineForEvent(eventId) { const event = this.findEventById(eventId); const thread = this.findThreadForEvent(event); - if (thread) { - return thread.timelineSet.getLiveTimeline(); + return thread.timelineSet.getTimelineForEvent(eventId); } else { return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId); } } + /** * Add a new timeline to this room's unfiltered timeline set * - * @return {module:models/event-timeline~EventTimeline} newly-created timeline + * @returns newly-created timeline */ - - addTimeline() { return this.getUnfilteredTimelineSet().addTimeline(); } + /** * Whether the timeline needs to be refreshed in order to pull in new * historical messages that were imported. - * @param {Boolean} value The value to set + * @param value - The value to set */ - - setTimelineNeedsRefresh(value) { this.timelineNeedsRefresh = value; } + /** * Whether the timeline needs to be refreshed in order to pull in new * historical messages that were imported. - * @return {Boolean} . + * @returns . */ - - getTimelineNeedsRefresh() { return this.timelineNeedsRefresh; } + /** * Get an event which is stored in our unfiltered timeline set, or in a thread * - * @param {string} eventId event ID to look for - * @return {?module:models/event.MatrixEvent} the given event, or undefined if unknown + * @param eventId - event ID to look for + * @returns the given event, or undefined if unknown */ - - findEventById(eventId) { let event = this.getUnfilteredTimelineSet().findEventById(eventId); - if (!event) { const threads = this.getThreads(); - for (let i = 0; i < threads.length; i++) { const thread = threads[i]; event = thread.findEventById(eventId); - if (event) { return event; } } } - return event; } + /** * Get one of the notification counts for this room - * @param {String} type The type of notification count to get. default: 'total' - * @return {Number} The notification count, or undefined if there is no count + * @param type - The type of notification count to get. default: 'total' + * @returns The notification count, or undefined if there is no count * for this type. */ + getUnreadNotificationCount(type = NotificationCountType.Total) { + let count = this.getRoomUnreadNotificationCount(type); + for (const threadNotification of this.threadNotifications.values()) { + count += threadNotification[type] ?? 0; + } + return count; + } + /** + * Get the notification for the event context (room or thread timeline) + */ + getUnreadCountForEventContext(type = NotificationCountType.Total, event) { + const isThreadEvent = !!event.threadRootId && !event.isThreadRoot; + return (isThreadEvent ? this.getThreadUnreadNotificationCount(event.threadRootId, type) : this.getRoomUnreadNotificationCount(type)) ?? 0; + } - getUnreadNotificationCount(type = NotificationCountType.Total) { - return this.notificationCounts[type]; + /** + * Get one of the notification counts for this room + * @param type - The type of notification count to get. default: 'total' + * @returns The notification count, or undefined if there is no count + * for this type. + */ + getRoomUnreadNotificationCount(type = NotificationCountType.Total) { + return this.notificationCounts[type] ?? 0; } + /** - * Set one of the notification counts for this room - * @param {String} type The type of notification count to set. - * @param {Number} count The new count + * Get one of the notification counts for a thread + * @param threadId - the root event ID + * @param type - The type of notification count to get. default: 'total' + * @returns The notification count, or undefined if there is no count + * for this type. */ + getThreadUnreadNotificationCount(threadId, type = NotificationCountType.Total) { + return this.threadNotifications.get(threadId)?.[type] ?? 0; + } + + /** + * Checks if the current room has unread thread notifications + * @returns + */ + hasThreadUnreadNotification() { + for (const notification of this.threadNotifications.values()) { + if ((notification.highlight ?? 0) > 0 || (notification.total ?? 0) > 0) { + return true; + } + } + return false; + } + + /** + * Swet one of the notification count for a thread + * @param threadId - the root event ID + * @param type - The type of notification count to get. default: 'total' + * @returns + */ + setThreadUnreadNotificationCount(threadId, type, count) { + const notification = _objectSpread({ + highlight: this.threadNotifications.get(threadId)?.highlight, + total: this.threadNotifications.get(threadId)?.total + }, { + [type]: count + }); + this.threadNotifications.set(threadId, notification); + this.emit(RoomEvent.UnreadNotifications, notification, threadId); + } + + /** + * @returns the notification count type for all the threads in the room + */ + get threadsAggregateNotificationType() { + let type = null; + for (const threadNotification of this.threadNotifications.values()) { + if ((threadNotification.highlight ?? 0) > 0) { + return NotificationCountType.Highlight; + } else if ((threadNotification.total ?? 0) > 0 && !type) { + type = NotificationCountType.Total; + } + } + return type; + } + /** + * Resets the thread notifications for this room + */ + resetThreadUnreadNotificationCount(notificationsToKeep) { + if (notificationsToKeep) { + for (const [threadId] of this.threadNotifications) { + if (!notificationsToKeep.includes(threadId)) { + this.threadNotifications.delete(threadId); + } + } + } else { + this.threadNotifications.clear(); + } + this.emit(RoomEvent.UnreadNotifications); + } + /** + * Set one of the notification counts for this room + * @param type - The type of notification count to set. + * @param count - The new count + */ setUnreadNotificationCount(type, count) { this.notificationCounts[type] = count; + this.emit(RoomEvent.UnreadNotifications, this.notificationCounts); + } + setUnread(type, count) { + return this.setUnreadNotificationCount(type, count); } - setSummary(summary) { const heroes = summary["m.heroes"]; const joinedCount = summary["m.joined_member_count"]; const invitedCount = summary["m.invited_member_count"]; - if (Number.isInteger(joinedCount)) { this.currentState.setJoinedMemberCount(joinedCount); } - if (Number.isInteger(invitedCount)) { this.currentState.setInvitedMemberCount(invitedCount); } - if (Array.isArray(heroes)) { // be cautious about trusting server values, // and make sure heroes doesn't contain our own id @@ -1238,302 +1184,241 @@ }); } } + /** * Whether to send encrypted messages to devices within this room. - * @param {Boolean} value true to blacklist unverified devices, null + * @param value - true to blacklist unverified devices, null * to use the global value for this room. */ - - setBlacklistUnverifiedDevices(value) { this.blacklistUnverifiedDevices = value; } + /** * Whether to send encrypted messages to devices within this room. - * @return {Boolean} true if blacklisting unverified devices, null + * @returns true if blacklisting unverified devices, null * if the global value should be used for this room. */ - - getBlacklistUnverifiedDevices() { + if (this.blacklistUnverifiedDevices === undefined) return null; return this.blacklistUnverifiedDevices; } + /** * Get the avatar URL for a room if one was set. - * @param {String} baseUrl The homeserver base URL. See - * {@link module:client~MatrixClient#getHomeserverUrl}. - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param baseUrl - The homeserver base URL. See + * {@link MatrixClient#getHomeserverUrl}. + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {boolean} allowDefault True to allow an identicon for this room if an + * @param allowDefault - True to allow an identicon for this room if an * avatar URL wasn't explicitly set. Default: true. (Deprecated) - * @return {?string} the avatar URL or null. + * @returns the avatar URL or null. */ - - getAvatarUrl(baseUrl, width, height, resizeMethod, allowDefault = true) { const roomAvatarEvent = this.currentState.getStateEvents(_event2.EventType.RoomAvatar, ""); - if (!roomAvatarEvent && !allowDefault) { return null; } - const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null; - if (mainUrl) { return (0, _contentRepo.getHttpUriForMxc)(baseUrl, mainUrl, width, height, resizeMethod); } - return null; } + /** * Get the mxc avatar url for the room, if one was set. - * @return {string} the mxc avatar url or falsy + * @returns the mxc avatar url or falsy */ - - getMxcAvatarUrl() { return this.currentState.getStateEvents(_event2.EventType.RoomAvatar, "")?.getContent()?.url || null; } - /** - * Get the aliases this room has according to the room's state - * The aliases returned by this function may not necessarily - * still point to this room. - * @return {array} The room's alias as an array of strings - * @deprecated this uses m.room.aliases events, replaced by Room::getAltAliases() - */ - - - getAliases() { - const aliasStrings = []; - const aliasEvents = this.currentState.getStateEvents(_event2.EventType.RoomAliases); - - if (aliasEvents) { - for (const aliasEvent of aliasEvents) { - if (Array.isArray(aliasEvent.getContent().aliases)) { - const filteredAliases = aliasEvent.getContent().aliases.filter(a => { - if (typeof a !== "string") return false; - if (a[0] !== '#') return false; - if (!a.endsWith(`:${aliasEvent.getStateKey()}`)) return false; // It's probably valid by here. - return true; - }); - aliasStrings.push(...filteredAliases); - } - } - } - - return aliasStrings; - } /** * Get this room's canonical alias * The alias returned by this function may not necessarily * still point to this room. - * @return {?string} The room's canonical alias, or null if there is none + * @returns The room's canonical alias, or null if there is none */ - - getCanonicalAlias() { const canonicalAlias = this.currentState.getStateEvents(_event2.EventType.RoomCanonicalAlias, ""); - if (canonicalAlias) { return canonicalAlias.getContent().alias || null; } - return null; } + /** * Get this room's alternative aliases - * @return {array} The room's alternative aliases, or an empty array + * @returns The room's alternative aliases, or an empty array */ - - getAltAliases() { const canonicalAlias = this.currentState.getStateEvents(_event2.EventType.RoomCanonicalAlias, ""); - if (canonicalAlias) { return canonicalAlias.getContent().alt_aliases || []; } - return []; } + /** * Add events to a timeline * *

Will fire "Room.timeline" for each event added. * - * @param {MatrixEvent[]} events A list of events to add. + * @param events - A list of events to add. * - * @param {boolean} toStartOfTimeline True to add these events to the start + * @param toStartOfTimeline - True to add these events to the start * (oldest) instead of the end (newest) of the timeline. If true, the oldest * event will be the last element of 'events'. * - * @param {module:models/event-timeline~EventTimeline} timeline timeline to + * @param timeline - timeline to * add events to. * - * @param {string=} paginationToken token for the next batch of events - * - * @fires module:client~MatrixClient#event:"Room.timeline" + * @param paginationToken - token for the next batch of events * + * @remarks + * Fires {@link RoomEvent.Timeline} */ - - addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken) { timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken); } + /** - * @experimental + * Get the instance of the thread associated with the current event + * @param eventId - the ID of the current event + * @returns a thread instance if known */ - - getThread(eventId) { - return this.threads.get(eventId); + return this.threads.get(eventId) ?? null; } + /** - * @experimental + * Get all the known threads in the room */ - - getThreads() { return Array.from(this.threads.values()); } + /** * Get a member from the current room state. - * @param {string} userId The user ID of the member. - * @return {RoomMember} The member or null. + * @param userId - The user ID of the member. + * @returns The member or `null`. */ - - getMember(userId) { return this.currentState.getMember(userId); } + /** * Get all currently loaded members from the current * room state. - * @returns {RoomMember[]} Room members + * @returns Room members */ - - getMembers() { return this.currentState.getMembers(); } + /** * Get a list of members whose membership state is "join". - * @return {RoomMember[]} A list of currently joined members. + * @returns A list of currently joined members. */ - - getJoinedMembers() { return this.getMembersWithMembership("join"); } + /** * Returns the number of joined members in this room * This method caches the result. * This is a wrapper around the method of the same name in roomState, returning * its result for the room's current state. - * @return {number} The number of members in this room whose membership is 'join' + * @returns The number of members in this room whose membership is 'join' */ - - getJoinedMemberCount() { return this.currentState.getJoinedMemberCount(); } + /** * Returns the number of invited members in this room - * @return {number} The number of members in this room whose membership is 'invite' + * @returns The number of members in this room whose membership is 'invite' */ - - getInvitedMemberCount() { return this.currentState.getInvitedMemberCount(); } + /** * Returns the number of invited + joined members in this room - * @return {number} The number of members in this room whose membership is 'invite' or 'join' + * @returns The number of members in this room whose membership is 'invite' or 'join' */ - - getInvitedAndJoinedMemberCount() { return this.getInvitedMemberCount() + this.getJoinedMemberCount(); } + /** * Get a list of members with given membership state. - * @param {string} membership The membership state. - * @return {RoomMember[]} A list of members with the given membership state. + * @param membership - The membership state. + * @returns A list of members with the given membership state. */ - - getMembersWithMembership(membership) { return this.currentState.getMembers().filter(function (m) { return m.membership === membership; }); } + /** * Get a list of members we should be encrypting for in this room - * @return {Promise} A list of members who + * @returns A list of members who * we should encrypt messages for in this room. */ - - async getEncryptionTargetMembers() { await this.loadMembersIfNeeded(); let members = this.getMembersWithMembership("join"); - if (this.shouldEncryptForInvitedMembers()) { members = members.concat(this.getMembersWithMembership("invite")); } - return members; } + /** * Determine whether we should encrypt messages for invited users in this room - * @return {boolean} if we should encrypt messages for invited users + * @returns if we should encrypt messages for invited users */ - - shouldEncryptForInvitedMembers() { const ev = this.currentState.getStateEvents(_event2.EventType.RoomHistoryVisibility, ""); return ev?.getContent()?.history_visibility !== "joined"; } + /** * Get the default room name (i.e. what a given user would see if the * room had no m.room.name) - * @param {string} userId The userId from whose perspective we want + * @param userId - The userId from whose perspective we want * to calculate the default name - * @return {string} The default room name + * @returns The default room name */ - - getDefaultRoomName(userId) { return this.calculateRoomName(userId, true); } + /** * Check if the given user_id has the given membership state. - * @param {string} userId The user ID to check. - * @param {string} membership The membership e.g. 'join' - * @return {boolean} True if this user_id has the given membership state. + * @param userId - The user ID to check. + * @param membership - The membership e.g. `'join'` + * @returns True if this user_id has the given membership state. */ - - hasMembershipState(userId, membership) { const member = this.getMember(userId); - if (!member) { return false; } - return member.membership === membership; } + /** * Add a timelineSet for this room with the given filter - * @param {Filter} filter The filter to be applied to this timelineSet - * @param {Object=} opts Configuration options - * @param {*} opts.storageToken Optional. - * @return {EventTimelineSet} The timelineSet + * @param filter - The filter to be applied to this timelineSet + * @param opts - Configuration options + * @returns The timelineSet */ - - getOrCreateFilteredTimelineSet(filter, { prepopulateTimeline = true, useSyncEvents = true, @@ -1542,23 +1427,20 @@ if (this.filteredTimelineSets[filter.filterId]) { return this.filteredTimelineSets[filter.filterId]; } - const opts = Object.assign({ filter, pendingEvents }, this.opts); const timelineSet = new _eventTimelineSet.EventTimelineSet(this, opts); this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]); - if (useSyncEvents) { this.filteredTimelineSets[filter.filterId] = timelineSet; this.timelineSets.push(timelineSet); } - - const unfilteredLiveTimeline = this.getLiveTimeline(); // Not all filter are possible to replicate client-side only + const unfilteredLiveTimeline = this.getLiveTimeline(); + // Not all filter are possible to replicate client-side only // When that's the case we do not want to prepopulate from the live timeline // as we would get incorrect results compared to what the server would send back - if (prepopulateTimeline) { // populate up the new timelineSet with filtered events from our live // unfiltered timeline. @@ -1566,21 +1448,23 @@ // XXX: This is risky as our timeline // may have grown huge and so take a long time to filter. // see https://github.com/vector-im/vector-web/issues/2109 + unfilteredLiveTimeline.getEvents().forEach(function (event) { timelineSet.addLiveEvent(event); - }); // find the earliest unfiltered timeline + }); + // find the earliest unfiltered timeline let timeline = unfilteredLiveTimeline; - while (timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS)) { timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS); } - timelineSet.getLiveTimeline().setPaginationToken(timeline.getPaginationToken(_eventTimeline.EventTimeline.BACKWARDS), _eventTimeline.EventTimeline.BACKWARDS); } else if (useSyncEvents) { const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(_eventTimeline.Direction.Forward); timelineSet.getLiveTimeline().setPaginationToken(livePaginationToken, _eventTimeline.Direction.Backward); - } // alternatively, we could try to do something like this to try and re-paginate + } + + // alternatively, we could try to do something like this to try and re-paginate // in the filtered events from nothing, but Mark says it's an abuse of the API // to do so: // @@ -1588,35 +1472,34 @@ // unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS) // ); - return timelineSet; } - async getThreadListFilter(filterType = _thread.ThreadFilterType.All) { const myUserId = this.client.getUserId(); const filter = new _filter.Filter(myUserId); const definition = { - "room": { - "timeline": { + room: { + timeline: { [_thread.FILTER_RELATED_BY_REL_TYPES.name]: [_thread.THREAD_RELATION_TYPE.name] } } }; - if (filterType === _thread.ThreadFilterType.My) { definition.room.timeline[_thread.FILTER_RELATED_BY_SENDERS.name] = [myUserId]; } - filter.setDefinition(definition); const filterId = await this.client.getOrCreateFilter(`THREAD_PANEL_${this.roomId}_${filterType}`, filter); filter.filterId = filterId; return filter; } - async createThreadTimelineSet(filterType) { let timelineSet; - - if (_thread.Thread.hasServerSideSupport) { + if (_thread.Thread.hasServerSideListSupport) { + timelineSet = new _eventTimelineSet.EventTimelineSet(this, _objectSpread(_objectSpread({}, this.opts), {}, { + pendingEvents: false + }), undefined, undefined, filterType ?? _thread.ThreadFilterType.All); + this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]); + } else if (_thread.Thread.hasServerSideSupport) { const filter = await this.getThreadListFilter(filterType); timelineSet = this.getOrCreateFilteredTimelineSet(filter, { prepopulateTimeline: false, @@ -1629,10 +1512,9 @@ }); Array.from(this.threads).forEach(([, thread]) => { if (thread.length === 0) return; - const currentUserParticipated = thread.events.some(event => { + const currentUserParticipated = thread.timeline.some(event => { return event.getSender() === this.client.getUserId(); }); - if (filterType !== _thread.ThreadFilterType.My || currentUserParticipated) { timelineSet.getLiveTimeline().addEvent(thread.rootEvent, { toStartOfTimeline: false @@ -1640,108 +1522,176 @@ } }); } - return timelineSet; } + /** + * Takes the given thread root events and creates threads for them. + */ + processThreadRoots(events, toStartOfTimeline) { + for (const rootEvent of events) { + _eventTimeline.EventTimeline.setEventMetadata(rootEvent, this.currentState, toStartOfTimeline); + if (!this.getThread(rootEvent.getId())) { + this.createThread(rootEvent.getId(), rootEvent, [], toStartOfTimeline); + } + } + } + /** + * Fetch the bare minimum of room threads required for the thread list to work reliably. + * With server support that means fetching one page. + * Without server support that means fetching as much at once as the server allows us to. + */ async fetchRoomThreads() { - if (this.threadsReady || !this.client.supportsExperimentalThreads()) { + if (this.threadsReady || !this.client.supportsThreads()) { return; } - - const allThreadsFilter = await this.getThreadListFilter(); - const { - chunk: events - } = await this.client.createMessagesRequest(this.roomId, "", Number.MAX_SAFE_INTEGER, _eventTimeline.Direction.Backward, allThreadsFilter); - if (!events.length) return; // Sorted by last_reply origin_server_ts - - const threadRoots = events.map(this.client.getEventMapper()).sort((eventA, eventB) => { - /** - * `origin_server_ts` in a decentralised world is far from ideal - * but for lack of any better, we will have to use this - * Long term the sorting should be handled by homeservers and this - * is only meant as a short term patch - */ - const threadAMetadata = eventA.getServerAggregatedRelation(_event2.RelationType.Thread); - const threadBMetadata = eventB.getServerAggregatedRelation(_event2.RelationType.Thread); - return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts; - }); - let latestMyThreadsRootEvent; - const roomState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); - - for (const rootEvent of threadRoots) { - this.threadsTimelineSets[0].addLiveEvent(rootEvent, { - duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Ignore, - fromCache: false, - roomState + if (_thread.Thread.hasServerSideListSupport) { + await Promise.all([this.fetchRoomThreadList(_thread.ThreadFilterType.All), this.fetchRoomThreadList(_thread.ThreadFilterType.My)]); + } else { + const allThreadsFilter = await this.getThreadListFilter(); + const { + chunk: events + } = await this.client.createMessagesRequest(this.roomId, "", Number.MAX_SAFE_INTEGER, _eventTimeline.Direction.Backward, allThreadsFilter); + if (!events.length) return; + + // Sorted by last_reply origin_server_ts + const threadRoots = events.map(this.client.getEventMapper()).sort((eventA, eventB) => { + /** + * `origin_server_ts` in a decentralised world is far from ideal + * but for lack of any better, we will have to use this + * Long term the sorting should be handled by homeservers and this + * is only meant as a short term patch + */ + const threadAMetadata = eventA.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name); + const threadBMetadata = eventB.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name); + return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts; }); - const threadRelationship = rootEvent.getServerAggregatedRelation(_event2.RelationType.Thread); - - if (threadRelationship.current_user_participated) { - this.threadsTimelineSets[1].addLiveEvent(rootEvent, { + let latestMyThreadsRootEvent; + const roomState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); + for (const rootEvent of threadRoots) { + const opts = { duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Ignore, fromCache: false, roomState - }); - latestMyThreadsRootEvent = rootEvent; + }; + this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts); + const threadRelationship = rootEvent.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name); + if (threadRelationship?.current_user_participated) { + this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, opts); + latestMyThreadsRootEvent = rootEvent; + } } - - if (!this.getThread(rootEvent.getId())) { - this.createThread(rootEvent.getId(), rootEvent, [], true); + this.processThreadRoots(threadRoots, true); + this.client.decryptEventIfNeeded(threadRoots[threadRoots.length - 1]); + if (latestMyThreadsRootEvent) { + this.client.decryptEventIfNeeded(latestMyThreadsRootEvent); } } + this.on(_thread.ThreadEvent.NewReply, this.onThreadNewReply); + this.on(_thread.ThreadEvent.Delete, this.onThreadDelete); + this.threadsReady = true; + } + async processPollEvents(events) { + const processPollStartEvent = event => { + if (!_matrixEventsSdk.M_POLL_START.matches(event.getType())) return; + try { + const poll = new _poll.Poll(event, this.client, this); + this.polls.set(event.getId(), poll); + this.emit(_poll.PollEvent.New, poll); + } catch {} + // poll creation can fail for malformed poll start events + }; - this.client.decryptEventIfNeeded(threadRoots[threadRoots.length - 1]); - - if (latestMyThreadsRootEvent) { - this.client.decryptEventIfNeeded(latestMyThreadsRootEvent); + const processPollRelationEvent = event => { + const relationEventId = event.relationEventId; + if (relationEventId && this.polls.has(relationEventId)) { + const poll = this.polls.get(relationEventId); + poll?.onNewRelation(event); + } + }; + const processPollEvent = event => { + processPollStartEvent(event); + processPollRelationEvent(event); + }; + for (const event of events) { + try { + await this.client.decryptEventIfNeeded(event); + processPollEvent(event); + } catch {} } - - this.threadsReady = true; - this.on(_thread.ThreadEvent.NewReply, this.onThreadNewReply); } + /** + * Fetch a single page of threadlist messages for the specific thread filter + * @internal + */ + async fetchRoomThreadList(filter) { + const timelineSet = filter === _thread.ThreadFilterType.My ? this.threadsTimelineSets[1] : this.threadsTimelineSets[0]; + const { + chunk: events, + end + } = await this.client.createThreadListMessagesRequest(this.roomId, null, undefined, _eventTimeline.Direction.Backward, timelineSet.threadListType, timelineSet.getFilter()); + timelineSet.getLiveTimeline().setPaginationToken(end ?? null, _eventTimeline.Direction.Backward); + if (!events.length) return; + const matrixEvents = events.map(this.client.getEventMapper()); + this.processThreadRoots(matrixEvents, true); + const roomState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); + for (const rootEvent of matrixEvents) { + timelineSet.addLiveEvent(rootEvent, { + duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Replace, + fromCache: false, + roomState + }); + } + } onThreadNewReply(thread) { + this.updateThreadRootEvents(thread, false, true); + } + onThreadDelete(thread) { + this.threads.delete(thread.id); + const timeline = this.getTimelineForEvent(thread.id); + const roomEvent = timeline?.getEvents()?.find(it => it.getId() === thread.id); + if (roomEvent) { + thread.clearEventMetadata(roomEvent); + } else { + _logger.logger.debug("onThreadDelete: Could not find root event in room timeline"); + } for (const timelineSet of this.threadsTimelineSets) { timelineSet.removeEvent(thread.id); - timelineSet.addLiveEvent(thread.rootEvent); } } + /** * Forget the timelineSet for this room with the given filter * - * @param {Filter} filter the filter whose timelineSet is to be forgotten + * @param filter - the filter whose timelineSet is to be forgotten */ - - removeFilteredTimelineSet(filter) { const timelineSet = this.filteredTimelineSets[filter.filterId]; delete this.filteredTimelineSets[filter.filterId]; const i = this.timelineSets.indexOf(timelineSet); - if (i > -1) { this.timelineSets.splice(i, 1); } } - eventShouldLiveIn(event, events, roots) { - if (!this.client.supportsExperimentalThreads()) { + if (!this.client?.supportsThreads()) { return { shouldLiveInRoom: true, shouldLiveInThread: false }; - } // A thread root is always shown in both timelines - + } + // A thread root is always shown in both timelines if (event.isThreadRoot || roots?.has(event.getId())) { return { shouldLiveInRoom: true, shouldLiveInThread: true, threadId: event.getId() }; - } // A thread relation is always only shown in a thread - + } + // A thread relation is always only shown in a thread if (event.isRelation(_thread.THREAD_RELATION_TYPE.name)) { return { shouldLiveInRoom: false, @@ -1749,30 +1699,29 @@ threadId: event.threadRootId }; } - const parentEventId = event.getAssociatedId(); - const parentEvent = this.findEventById(parentEventId) ?? events?.find(e => e.getId() === parentEventId); // Treat relations and redactions as extensions of their parents so evaluate parentEvent instead + const parentEvent = this.findEventById(parentEventId) ?? events?.find(e => e.getId() === parentEventId); + // Treat relations and redactions as extensions of their parents so evaluate parentEvent instead if (parentEvent && (event.isRelation() || event.isRedaction())) { return this.eventShouldLiveIn(parentEvent, events, roots); - } // Edge case where we know the event is a relation but don't have the parentEvent - + } + // Edge case where we know the event is a relation but don't have the parentEvent if (roots?.has(event.relationEventId)) { return { shouldLiveInRoom: true, shouldLiveInThread: true, threadId: event.relationEventId }; - } // We've exhausted all scenarios, can safely assume that this event should live in the room timeline only - + } + // We've exhausted all scenarios, can safely assume that this event should live in the room timeline only return { shouldLiveInRoom: true, shouldLiveInThread: false }; } - findThreadForEvent(event) { if (!event) return null; const { @@ -1780,112 +1729,101 @@ } = this.eventShouldLiveIn(event); return threadId ? this.getThread(threadId) : null; } - addThreadedEvents(threadId, events, toStartOfTimeline = false) { let thread = this.getThread(threadId); - - if (thread) { - thread.addEvents(events, toStartOfTimeline); - } else { + if (!thread) { const rootEvent = this.findEventById(threadId) ?? events.find(e => e.getId() === threadId); thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline); - this.emit(_thread.ThreadEvent.Update, thread); } + thread.addEvents(events, toStartOfTimeline); } + /** * Adds events to a thread's timeline. Will fire "Thread.update" - * @experimental */ - - processThreadedEvents(events, toStartOfTimeline) { events.forEach(this.applyRedaction); const eventsByThread = {}; - for (const event of events) { const { threadId, shouldLiveInThread } = this.eventShouldLiveIn(event); - if (shouldLiveInThread && !eventsByThread[threadId]) { eventsByThread[threadId] = []; } - eventsByThread[threadId]?.push(event); } - Object.entries(eventsByThread).map(([threadId, threadEvents]) => this.addThreadedEvents(threadId, threadEvents, toStartOfTimeline)); } - createThread(threadId, rootEvent, events = [], toStartOfTimeline) { + if (this.threads.has(threadId)) { + return this.threads.get(threadId); + } if (rootEvent) { const relatedEvents = this.relations.getAllChildEventsForEvent(rootEvent.getId()); - if (relatedEvents?.length) { // Include all relations of the root event, given it'll be visible in both timelines, // except `m.replace` as that will already be applied atop the event using `MatrixEvent::makeReplaced` events = events.concat(relatedEvents.filter(e => !e.isRelation(_event2.RelationType.Replace))); } } - const thread = new _thread.Thread(threadId, rootEvent, { - initialEvents: events, room: this, - client: this.client - }); // If we managed to create a thread and figure out its `id` then we can use it + client: this.client, + pendingEventOrdering: this.opts.pendingEventOrdering, + receipts: this.cachedThreadReadReceipts.get(threadId) ?? [] + }); + // All read receipts should now come down from sync, we do not need to keep + // a reference to the cached receipts anymore. + this.cachedThreadReadReceipts.delete(threadId); + + // If we managed to create a thread and figure out its `id` then we can use it + // This has to happen before thread.addEvents, because that adds events to the eventtimeline, and the + // eventtimeline sometimes looks up thread information via the room. this.threads.set(thread.id, thread); - this.reEmitter.reEmit(thread, [_thread.ThreadEvent.Update, _thread.ThreadEvent.NewReply, RoomEvent.Timeline, RoomEvent.TimelineReset]); - if (!this.lastThread || this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp) { + // This is necessary to be able to jump to events in threads: + // If we jump to an event in a thread where neither the event, nor the root, + // nor any thread event are loaded yet, we'll load the event as well as the thread root, create the thread, + // and pass the event through this. + thread.addEvents(events, false); + this.reEmitter.reEmit(thread, [_thread.ThreadEvent.Delete, _thread.ThreadEvent.Update, _thread.ThreadEvent.NewReply, RoomEvent.Timeline, RoomEvent.TimelineReset]); + const isNewer = this.lastThread?.rootEvent && rootEvent?.localTimestamp && this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp; + if (!this.lastThread || isNewer) { this.lastThread = thread; } - - this.emit(_thread.ThreadEvent.New, thread, toStartOfTimeline); - if (this.threadsReady) { - this.threadsTimelineSets.forEach(timelineSet => { - if (thread.rootEvent) { - if (_thread.Thread.hasServerSideSupport) { - timelineSet.addLiveEvent(thread.rootEvent); - } else { - timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), toStartOfTimeline); - } - } - }); + this.updateThreadRootEvents(thread, toStartOfTimeline, false); } - + this.emit(_thread.ThreadEvent.New, thread, toStartOfTimeline); return thread; } - processLiveEvent(event) { - this.applyRedaction(event); // Implement MSC3531: hiding messages. + this.applyRedaction(event); + // Implement MSC3531: hiding messages. if (event.isVisibilityEvent()) { // This event changes the visibility of another event, record // the visibility change, inform clients if necessary. this.applyNewVisibilityEvent(event); - } // If any pending visibility change is waiting for this (older) event, - + } + // If any pending visibility change is waiting for this (older) event, + this.applyPendingVisibilityEvents(event); - this.applyPendingVisibilityEvents(event); // Sliding Sync modifications: + // Sliding Sync modifications: // The proxy cannot guarantee every sent event will have a transaction_id field, so we need // to check the event ID against the list of pending events if there is no transaction ID // field. Only do this for events sent by us though as it's potentially expensive to loop // the pending events map. - const txnId = event.getUnsigned().transaction_id; - if (!txnId && event.getSender() === this.myUserId) { // check the txn map for a matching event ID - for (const tid in this.txnToEvent) { - const localEvent = this.txnToEvent[tid]; - + for (const [tid, localEvent] of this.txnToEvent) { if (localEvent.getId() === event.getId()) { - _logger.logger.debug("processLiveEvent: found sent event without txn ID: ", tid, event.getId()); // update the unsigned field so we can re-use the same codepaths - - + _logger.logger.debug("processLiveEvent: found sent event without txn ID: ", tid, event.getId()); + // update the unsigned field so we can re-use the same codepaths const unsigned = event.getUnsigned(); unsigned.transaction_id = tid; event.setUnsigned(unsigned); @@ -1894,38 +1832,42 @@ } } } + /** * Add an event to the end of this room's live timelines. Will fire * "Room.timeline". * - * @param {MatrixEvent} event Event to be added - * @param {IAddLiveEventOptions} addLiveEventOptions addLiveEvent options - * @fires module:client~MatrixClient#event:"Room.timeline" - * @private + * @param event - Event to be added + * @param addLiveEventOptions - addLiveEvent options + * @internal + * + * @remarks + * Fires {@link RoomEvent.Timeline} */ - - addLiveEvent(event, addLiveEventOptions) { const { duplicateStrategy, timelineWasEmpty, fromCache - } = addLiveEventOptions; // add to our timeline sets + } = addLiveEventOptions; - for (let i = 0; i < this.timelineSets.length; i++) { - this.timelineSets[i].addLiveEvent(event, { + // add to our timeline sets + for (const timelineSet of this.timelineSets) { + timelineSet.addLiveEvent(event, { duplicateStrategy, fromCache, timelineWasEmpty }); - } // synthesize and inject implicit read receipts + } + + // synthesize and inject implicit read receipts // Done after adding the event because otherwise the app would get a read receipt // pointing to an event that wasn't yet in the timeline // Don't synthesize RR for m.room.redaction as this causes the RR to go missing. - - if (event.sender && event.getType() !== _event2.EventType.RoomRedaction) { - this.addReceipt(synthesizeReceipt(event.sender.userId, event, _read_receipts.ReceiptType.Read), true); // Any live events from a user could be taken as implicit + this.addReceipt((0, _readReceipt.synthesizeReceipt)(event.sender.userId, event, _read_receipts.ReceiptType.Read), true); + + // Any live events from a user could be taken as implicit // presence information: evidence that they are currently active. // ...except in a world where we use 'user.currentlyActive' to reduce // presence spam, this isn't very useful - we'll get a transition when @@ -1933,6 +1875,7 @@ // reset the lastActiveAgo and lastPresenceTs from the RoomState's user. } } + /** * Add a pending outgoing event to this room. * @@ -1941,67 +1884,55 @@ * *

This is an internal method, intended for use by MatrixClient. * - * @param {module:models/event.MatrixEvent} event The event to add. - * - * @param {string} txnId Transaction id for this outgoing event + * @param event - The event to add. * - * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * @param txnId - Transaction id for this outgoing event * * @throws if the event doesn't have status SENDING, or we aren't given a * unique transaction id. + * + * @remarks + * Fires {@link RoomEvent.LocalEchoUpdated} */ - - addPendingEvent(event, txnId) { if (event.status !== _eventStatus.EventStatus.SENDING && event.status !== _eventStatus.EventStatus.NOT_SENT) { throw new Error("addPendingEvent called on an event with status " + event.status); } - - if (this.txnToEvent[txnId]) { + if (this.txnToEvent.get(txnId)) { throw new Error("addPendingEvent called on an event with known txnId " + txnId); - } // call setEventMetadata to set up event.sender etc + } + + // call setEventMetadata to set up event.sender etc // as event is shared over all timelineSets, we set up its metadata based // on the unfiltered timelineSet. - - _eventTimeline.EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS), false); - - this.txnToEvent[txnId] = event; - - if (this.opts.pendingEventOrdering === _client.PendingEventOrdering.Detached) { + this.txnToEvent.set(txnId, event); + if (this.pendingEventList) { if (this.pendingEventList.some(e => e.status === _eventStatus.EventStatus.NOT_SENT)) { _logger.logger.warn("Setting event as NOT_SENT due to messages in the same state"); - event.setStatus(_eventStatus.EventStatus.NOT_SENT); } - this.pendingEventList.push(event); this.savePendingEvents(); - if (event.isRelation()) { // For pending events, add them to the relations collection immediately. // (The alternate case below already covers this as part of adding to // the timeline set.) this.aggregateNonLiveRelation(event); } - if (event.isRedaction()) { const redactId = event.event.redacts; - let redactedEvent = this.pendingEventList?.find(e => e.getId() === redactId); - - if (!redactedEvent) { + let redactedEvent = this.pendingEventList.find(e => e.getId() === redactId); + if (!redactedEvent && redactId) { redactedEvent = this.findEventById(redactId); } - if (redactedEvent) { redactedEvent.markLocallyRedacted(event); this.emit(RoomEvent.Redaction, event, this); } } } else { - for (let i = 0; i < this.timelineSets.length; i++) { - const timelineSet = this.timelineSets[i]; - + for (const timelineSet of this.timelineSets) { if (timelineSet.getFilter()) { if (timelineSet.getFilter().filterRoomTimeline([event]).length) { timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), { @@ -2015,9 +1946,9 @@ } } } - - this.emit(RoomEvent.LocalEchoUpdated, event, this, null, null); + this.emit(RoomEvent.LocalEchoUpdated, event, this); } + /** * Persists all pending events to local storage * @@ -2025,14 +1956,12 @@ * all messages that are not yet encrypted will be discarded * * This is because the flow of EVENT_STATUS transition is - * queued => sending => encrypting => sending => sent + * `queued => sending => encrypting => sending => sent` * * Steps 3 and 4 are skipped for unencrypted room. * It is better to discard an unencrypted message rather than persisting * it locally for everyone to read */ - - savePendingEvents() { if (this.pendingEventList) { const pendingEvents = this.pendingEventList.map(event => { @@ -2048,6 +1977,7 @@ this.client.store.setPendingEvents(this.roomId, pendingEvents); } } + /** * Used to aggregate the local echo for a relation, and also * for re-applying a relation after it's redaction has been cancelled, @@ -2056,132 +1986,119 @@ * which are just kept detached for their local echo. * * Also note that live events are aggregated in the live EventTimelineSet. - * @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated. + * @param event - the relation event that needs to be aggregated. */ - - aggregateNonLiveRelation(event) { this.relations.aggregateChildEvent(event); } - getEventForTxnId(txnId) { - return this.txnToEvent[txnId]; + return this.txnToEvent.get(txnId); } + /** * Deal with the echo of a message we sent. * *

We move the event to the live timeline if it isn't there already, and * update it. * - * @param {module:models/event.MatrixEvent} remoteEvent The event received from + * @param remoteEvent - The event received from * /sync - * @param {module:models/event.MatrixEvent} localEvent The local echo, which + * @param localEvent - The local echo, which * should be either in the pendingEventList or the timeline. * - * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" - * @private + * @internal + * + * @remarks + * Fires {@link RoomEvent.LocalEchoUpdated} */ - - handleRemoteEcho(remoteEvent, localEvent) { const oldEventId = localEvent.getId(); const newEventId = remoteEvent.getId(); const oldStatus = localEvent.status; + _logger.logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`); - _logger.logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`); // no longer pending - - - delete this.txnToEvent[remoteEvent.getUnsigned().transaction_id]; // if it's in the pending list, remove it + // no longer pending + this.txnToEvent.delete(remoteEvent.getUnsigned().transaction_id); + // if it's in the pending list, remove it if (this.pendingEventList) { this.removePendingEvent(oldEventId); - } // replace the event source (this will preserve the plaintext payload if - // any, which is good, because we don't want to try decoding it again). - + } + // replace the event source (this will preserve the plaintext payload if + // any, which is good, because we don't want to try decoding it again). localEvent.handleRemoteEcho(remoteEvent.event); const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent); - const thread = this.getThread(threadId); + const thread = threadId ? this.getThread(threadId) : null; + thread?.setEventMetadata(localEvent); thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); - if (shouldLiveInRoom) { - for (let i = 0; i < this.timelineSets.length; i++) { - const timelineSet = this.timelineSets[i]; // if it's already in the timeline, update the timeline map. If it's not, add it. - + for (const timelineSet of this.timelineSets) { + // if it's already in the timeline, update the timeline map. If it's not, add it. timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId); } } - this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus); } + /** * Update the status / event id on a pending event, to reflect its transmission * progress. * *

This is an internal method. * - * @param {MatrixEvent} event local echo event - * @param {EventStatus} newStatus status to assign - * @param {string} newEventId new event id to assign. Ignored unless - * newStatus == EventStatus.SENT. - * @fires module:client~MatrixClient#event:"Room.localEchoUpdated" + * @param event - local echo event + * @param newStatus - status to assign + * @param newEventId - new event id to assign. Ignored unless newStatus == EventStatus.SENT. + * + * @remarks + * Fires {@link RoomEvent.LocalEchoUpdated} */ - - updatePendingEvent(event, newStatus, newEventId) { - _logger.logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` + `event ID ${event.getId()} -> ${newEventId}`); // if the message was sent, we expect an event id - + _logger.logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` + `event ID ${event.getId()} -> ${newEventId}`); + // if the message was sent, we expect an event id if (newStatus == _eventStatus.EventStatus.SENT && !newEventId) { - throw new Error("updatePendingEvent called with status=SENT, " + "but no new event id"); - } // SENT races against /sync, so we have to special-case it. - + throw new Error("updatePendingEvent called with status=SENT, but no new event id"); + } + // SENT races against /sync, so we have to special-case it. if (newStatus == _eventStatus.EventStatus.SENT) { const timeline = this.getTimelineForEvent(newEventId); - if (timeline) { // we've already received the event via the event stream. // nothing more to do here, assuming the transaction ID was correctly matched. // Let's check that. const remoteEvent = this.findEventById(newEventId); - const remoteTxnId = remoteEvent.getUnsigned().transaction_id; - - if (!remoteTxnId) { + const remoteTxnId = remoteEvent?.getUnsigned().transaction_id; + if (!remoteTxnId && remoteEvent) { // This code path is mostly relevant for the Sliding Sync proxy. // The remote event did not contain a transaction ID, so we did not handle // the remote echo yet. Handle it now. const unsigned = remoteEvent.getUnsigned(); unsigned.transaction_id = event.getTxnId(); - remoteEvent.setUnsigned(unsigned); // the remote event is _already_ in the timeline, so we need to remove it so + remoteEvent.setUnsigned(unsigned); + // the remote event is _already_ in the timeline, so we need to remove it so // we can convert the local event into the final event. - this.removeEvent(remoteEvent.getId()); this.handleRemoteEcho(remoteEvent, event); } - return; } } - const oldStatus = event.status; const oldEventId = event.getId(); - if (!oldStatus) { - throw new Error("updatePendingEventStatus called on an event which is " + "not a local echo."); + throw new Error("updatePendingEventStatus called on an event which is not a local echo."); } - const allowed = ALLOWED_TRANSITIONS[oldStatus]; - - if (!allowed || allowed.indexOf(newStatus) < 0) { - throw new Error("Invalid EventStatus transition " + oldStatus + "->" + newStatus); + if (!allowed?.includes(newStatus)) { + throw new Error(`Invalid EventStatus transition ${oldStatus}->${newStatus}`); } - event.setStatus(newStatus); - if (newStatus == _eventStatus.EventStatus.SENT) { // update the event id event.replaceLocalEventId(newEventId); @@ -2189,15 +2106,15 @@ shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event); - const thread = this.getThread(threadId); + const thread = threadId ? this.getThread(threadId) : undefined; + thread?.setEventMetadata(event); thread?.timelineSet.replaceEventId(oldEventId, newEventId); - if (shouldLiveInRoom) { // if the event was already in the timeline (which will be the case if // opts.pendingEventOrdering==chronological), we need to update the // timeline map. - for (let i = 0; i < this.timelineSets.length; i++) { - this.timelineSets[i].replaceEventId(oldEventId, newEventId); + for (const timelineSet of this.timelineSets) { + timelineSet.replaceEventId(oldEventId, newEventId); } } } else if (newStatus == _eventStatus.EventStatus.CANCELLED) { @@ -2205,84 +2122,71 @@ if (this.pendingEventList) { const removedEvent = this.getPendingEvent(oldEventId); this.removePendingEvent(oldEventId); - - if (removedEvent.isRedaction()) { + if (removedEvent?.isRedaction()) { this.revertRedactionLocalEcho(removedEvent); } } - this.removeEvent(oldEventId); } - this.savePendingEvents(); this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus); } - revertRedactionLocalEcho(redactionEvent) { const redactId = redactionEvent.event.redacts; - if (!redactId) { return; } - const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); - if (redactedEvent) { - redactedEvent.unmarkLocallyRedacted(); // re-render after undoing redaction - - this.emit(RoomEvent.RedactionCancelled, redactionEvent, this); // reapply relation now redaction failed - + redactedEvent.unmarkLocallyRedacted(); + // re-render after undoing redaction + this.emit(RoomEvent.RedactionCancelled, redactionEvent, this); + // reapply relation now redaction failed if (redactedEvent.isRelation()) { this.aggregateNonLiveRelation(redactedEvent); } } } + /** * Add some events to this room. This can include state events, message * events and typing notifications. These events are treated as "live" so * they will go to the end of the timeline. * - * @param {MatrixEvent[]} events A list of events to add. - * @param {IAddLiveEventOptions} options addLiveEvent options - * @throws If duplicateStrategy is not falsey, 'replace' or 'ignore'. + * @param events - A list of events to add. + * @param addLiveEventOptions - addLiveEvent options + * @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'. */ - addLiveEvents(events, duplicateStrategyOrOpts, fromCache = false) { let duplicateStrategy = duplicateStrategyOrOpts; let timelineWasEmpty = false; - - if (typeof duplicateStrategyOrOpts === 'object') { + if (typeof duplicateStrategyOrOpts === "object") { ({ duplicateStrategy, fromCache = false, - /* roomState, (not used here) */ timelineWasEmpty } = duplicateStrategyOrOpts); } else if (duplicateStrategyOrOpts !== undefined) { // Deprecation warning // FIXME: Remove after 2023-06-01 (technical debt) - _logger.logger.warn('Overload deprecated: ' + '`Room.addLiveEvents(events, duplicateStrategy?, fromCache?)` ' + 'is deprecated in favor of the overload with `Room.addLiveEvents(events, IAddLiveEventOptions)`'); + _logger.logger.warn("Overload deprecated: " + "`Room.addLiveEvents(events, duplicateStrategy?, fromCache?)` " + "is deprecated in favor of the overload with `Room.addLiveEvents(events, IAddLiveEventOptions)`"); } - if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) { throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'"); - } // sanity check that the live timeline is still live - + } + // sanity check that the live timeline is still live for (let i = 0; i < this.timelineSets.length; i++) { const liveTimeline = this.timelineSets[i].getLiveTimeline(); - if (liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS)) { throw new Error("live timeline " + i + " is no longer live - it has a pagination token " + "(" + liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS) + ")"); } - if (liveTimeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS)) { throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`); } } - const threadRoots = this.findThreadRoots(events); const eventsByThread = {}; const options = { @@ -2290,14 +2194,11 @@ fromCache, timelineWasEmpty }; - for (const event of events) { // TODO: We should have a filter to say "only add state event types X Y Z to the timeline". this.processLiveEvent(event); - if (event.getUnsigned().transaction_id) { - const existingEvent = this.txnToEvent[event.getUnsigned().transaction_id]; - + const existingEvent = this.txnToEvent.get(event.getUnsigned().transaction_id); if (existingEvent) { // remote echo of an event we sent earlier this.handleRemoteEcho(event, existingEvent); @@ -2310,29 +2211,23 @@ shouldLiveInThread, threadId } = this.eventShouldLiveIn(event, events, threadRoots); - - if (shouldLiveInThread && !eventsByThread[threadId]) { - eventsByThread[threadId] = []; + if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) { + eventsByThread[threadId ?? ""] = []; } - - eventsByThread[threadId]?.push(event); - + eventsByThread[threadId ?? ""]?.push(event); if (shouldLiveInRoom) { this.addLiveEvent(event, options); } } - Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => { this.addThreadedEvents(threadId, threadEvents, false); }); } - partitionThreadedEvents(events) { // Indices to the events array, for readability const ROOM = 0; const THREAD = 1; - - if (this.client.supportsExperimentalThreads()) { + if (this.client.supportsThreads()) { const threadRoots = this.findThreadRoots(events); return events.reduce((memo, event) => { const { @@ -2340,16 +2235,13 @@ shouldLiveInThread, threadId } = this.eventShouldLiveIn(event, events, threadRoots); - if (shouldLiveInRoom) { memo[ROOM].push(event); } - if (shouldLiveInThread) { - event.setThreadId(threadId); + event.setThreadId(threadId ?? ""); memo[THREAD].push(event); } - return memo; }, [[], []]); } else { @@ -2357,28 +2249,85 @@ return [events, []]; } } + /** * Given some events, find the IDs of all the thread roots that are referred to by them. */ - - findThreadRoots(events) { const threadRoots = new Set(); - for (const event of events) { if (event.isRelation(_thread.THREAD_RELATION_TYPE.name)) { - threadRoots.add(event.relationEventId); + threadRoots.add(event.relationEventId ?? ""); } } - return threadRoots; } + /** - * Adds/handles ephemeral events such as typing notifications and read receipts. - * @param {MatrixEvent[]} events A list of events to process + * Add a receipt event to the room. + * @param event - The m.receipt event. + * @param synthetic - True if this event is implicit. */ + addReceipt(event, synthetic = false) { + const content = event.getContent(); + Object.keys(content).forEach(eventId => { + Object.keys(content[eventId]).forEach(receiptType => { + Object.keys(content[eventId][receiptType]).forEach(userId => { + const receipt = content[eventId][receiptType][userId]; + const receiptForMainTimeline = !receipt.thread_id || receipt.thread_id === _read_receipts.MAIN_ROOM_TIMELINE; + const receiptDestination = receiptForMainTimeline ? this : this.threads.get(receipt.thread_id ?? ""); + if (receiptDestination) { + receiptDestination.addReceiptToStructure(eventId, receiptType, userId, receipt, synthetic); + + // If the read receipt sent for the logged in user matches + // the last event of the live timeline, then we know for a fact + // that the user has read that message. + // We can mark the room as read and not wait for the local echo + // from synapse + // This needs to be done after the initial sync as we do not want this + // logic to run whilst the room is being initialised + if (this.client.isInitialSyncComplete() && userId === this.client.getUserId()) { + const lastEvent = receiptDestination.timeline[receiptDestination.timeline.length - 1]; + if (lastEvent && eventId === lastEvent.getId() && userId === lastEvent.getSender()) { + receiptDestination.setUnread(NotificationCountType.Total, 0); + receiptDestination.setUnread(NotificationCountType.Highlight, 0); + } + } + } else { + // The thread does not exist locally, keep the read receipt + // in a cache locally, and re-apply the `addReceipt` logic + // when the thread is created + this.cachedThreadReadReceipts.set(receipt.thread_id, [...(this.cachedThreadReadReceipts.get(receipt.thread_id) ?? []), { + eventId, + receiptType, + userId, + receipt, + synthetic + }]); + } + const me = this.client.getUserId(); + // Track the time of the current user's oldest threaded receipt in the room. + if (userId === me && !receiptForMainTimeline && receipt.ts < this.oldestThreadedReceiptTs) { + this.oldestThreadedReceiptTs = receipt.ts; + } + // Track each user's unthreaded read receipt. + if (!receipt.thread_id && receipt.ts > (this.unthreadedReceipts.get(userId)?.ts ?? 0)) { + this.unthreadedReceipts.set(userId, receipt); + } + }); + }); + }); + // send events after we've regenerated the structure & cache, otherwise things that + // listened for the event would read stale data. + this.emit(RoomEvent.Receipt, event, this); + } + + /** + * Adds/handles ephemeral events such as typing notifications and read receipts. + * @param events - A list of events to process + */ addEphemeralEvents(events) { for (const event of events) { if (event.getType() === _event2.EventType.Typing) { @@ -2386,68 +2335,59 @@ } else if (event.getType() === _event2.EventType.Receipt) { this.addReceipt(event); } // else ignore - life is too short for us to care about these events - } } + /** * Removes events from this room. - * @param {String[]} eventIds A list of eventIds to remove. + * @param eventIds - A list of eventIds to remove. */ - - removeEvents(eventIds) { - for (let i = 0; i < eventIds.length; ++i) { - this.removeEvent(eventIds[i]); + for (const eventId of eventIds) { + this.removeEvent(eventId); } } + /** * Removes a single event from this room. * - * @param {String} eventId The id of the event to remove + * @param eventId - The id of the event to remove * - * @return {boolean} true if the event was removed from any of the room's timeline sets + * @returns true if the event was removed from any of the room's timeline sets */ - - removeEvent(eventId) { let removedAny = false; - - for (let i = 0; i < this.timelineSets.length; i++) { - const removed = this.timelineSets[i].removeEvent(eventId); - + for (const timelineSet of this.timelineSets) { + const removed = timelineSet.removeEvent(eventId); if (removed) { if (removed.isRedaction()) { this.revertRedactionLocalEcho(removed); } - removedAny = true; } } - return removedAny; } + /** * Recalculate various aspects of the room, including the room name and * room summary. Call this any time the room's current state is modified. * May fire "Room.name" if the room name is updated. - * @fires module:client~MatrixClient#event:"Room.name" + * + * @remarks + * Fires {@link RoomEvent.Name} */ - - recalculate() { // set fake stripped state events if this is an invite room so logic remains // consistent elsewhere. const membershipEvent = this.currentState.getStateEvents(_event2.EventType.RoomMember, this.myUserId); - if (membershipEvent) { const membership = membershipEvent.getContent().membership; this.updateMyMembership(membership); - if (membership === "invite") { const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || []; strippedStateEvents.forEach(strippedEvent => { const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key); - if (!existingEvent) { // set the fake stripped event instead this.currentState.setStateEvents([new _event.MatrixEvent({ @@ -2457,7 +2397,6 @@ event_id: "$fake" + Date.now(), room_id: this.roomId, user_id: this.myUserId // technically a lie - })]); } }); @@ -2470,248 +2409,15 @@ this.summary = new _roomSummary.RoomSummary(this.roomId, { title: this.name }); - if (oldName !== this.name) { this.emit(RoomEvent.Name, this); } } - /** - * Get a list of user IDs who have read up to the given event. - * @param {MatrixEvent} event the event to get read receipts for. - * @return {String[]} A list of user IDs. - */ - - - getUsersReadUpTo(event) { - return this.getReceiptsForEvent(event).filter(function (receipt) { - return utils.isSupportedReceiptType(receipt.type); - }).map(function (receipt) { - return receipt.userId; - }); - } - /** - * Gets the latest receipt for a given user in the room - * @param userId The id of the user for which we want the receipt - * @param ignoreSynthesized Whether to ignore synthesized receipts or not - * @param receiptType Optional. The type of the receipt we want to get - * @returns the latest receipts of the chosen type for the chosen user - */ - - - getReadReceiptForUserId(userId, ignoreSynthesized = false, receiptType = _read_receipts.ReceiptType.Read) { - const [realReceipt, syntheticReceipt] = this.receipts[receiptType]?.[userId] ?? []; - - if (ignoreSynthesized) { - return realReceipt; - } - - return syntheticReceipt ?? realReceipt; - } - /** - * Get the ID of the event that a given user has read up to, or null if we - * have received no read receipts from them. - * @param {String} userId The user ID to get read receipt event ID for - * @param {Boolean} ignoreSynthesized If true, return only receipts that have been - * sent by the server, not implicit ones generated - * by the JS SDK. - * @return {String} ID of the latest event that the given user has read, or null. - */ - - - getEventReadUpTo(userId, ignoreSynthesized = false) { - // XXX: This is very very ugly and I hope I won't have to ever add a new - // receipt type here again. IMHO this should be done by the server in - // some more intelligent manner or the client should just use timestamps - const timelineSet = this.getUnfilteredTimelineSet(); - const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, _read_receipts.ReceiptType.Read); - const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, _read_receipts.ReceiptType.ReadPrivate); // If we have both, compare them - - let comparison; - - if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) { - comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId); - } // If we didn't get a comparison try to compare the ts of the receipts - - - if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) { - comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts; - } // The public receipt is more likely to drift out of date so the private - // one has precedence - - if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null; // If public read receipt is older, return the private one - - return (comparison < 0 ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null; - } - /** - * Determines if the given user has read a particular event ID with the known - * history of the room. This is not a definitive check as it relies only on - * what is available to the room at the time of execution. - * @param {String} userId The user ID to check the read state of. - * @param {String} eventId The event ID to check if the user read. - * @returns {Boolean} True if the user has read the event, false otherwise. - */ - - - hasUserReadEvent(userId, eventId) { - const readUpToId = this.getEventReadUpTo(userId, false); - if (readUpToId === eventId) return true; - - if (this.timeline.length && this.timeline[this.timeline.length - 1].getSender() && this.timeline[this.timeline.length - 1].getSender() === userId) { - // It doesn't matter where the event is in the timeline, the user has read - // it because they've sent the latest event. - return true; - } - - for (let i = this.timeline.length - 1; i >= 0; --i) { - const ev = this.timeline[i]; // If we encounter the target event first, the user hasn't read it - // however if we encounter the readUpToId first then the user has read - // it. These rules apply because we're iterating bottom-up. - - if (ev.getId() === eventId) return false; - if (ev.getId() === readUpToId) return true; - } // We don't know if the user has read it, so assume not. - - - return false; - } - /** - * Get a list of receipts for the given event. - * @param {MatrixEvent} event the event to get receipts for - * @return {Object[]} A list of receipts with a userId, type and data keys or - * an empty list. - */ - - - getReceiptsForEvent(event) { - return this.receiptCacheByEventId[event.getId()] || []; - } - /** - * Add a receipt event to the room. - * @param {MatrixEvent} event The m.receipt event. - * @param {Boolean} synthetic True if this event is implicit. - */ - - - addReceipt(event, synthetic = false) { - this.addReceiptsToStructure(event, synthetic); // send events after we've regenerated the structure & cache, otherwise things that - // listened for the event would read stale data. - - this.emit(RoomEvent.Receipt, event, this); - } - /** - * Add a receipt event to the room. - * @param {MatrixEvent} event The m.receipt event. - * @param {Boolean} synthetic True if this event is implicit. - */ - - - addReceiptsToStructure(event, synthetic) { - const content = event.getContent(); - Object.keys(content).forEach(eventId => { - Object.keys(content[eventId]).forEach(receiptType => { - Object.keys(content[eventId][receiptType]).forEach(userId => { - const receipt = content[eventId][receiptType][userId]; - - if (!this.receipts[receiptType]) { - this.receipts[receiptType] = {}; - } - - if (!this.receipts[receiptType][userId]) { - this.receipts[receiptType][userId] = [null, null]; - } - - const pair = this.receipts[receiptType][userId]; - let existingReceipt = pair[ReceiptPairRealIndex]; - - if (synthetic) { - existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; - } - - if (existingReceipt) { - // we only want to add this receipt if we think it is later than the one we already have. - // This is managed server-side, but because we synthesize RRs locally we have to do it here too. - const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId); - - if (ordering !== null && ordering >= 0) { - return; - } - } - - const wrappedReceipt = { - eventId, - data: receipt - }; - const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt; - const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex]; - let ordering = null; - - if (realReceipt && syntheticReceipt) { - ordering = this.getUnfilteredTimelineSet().compareEventOrdering(realReceipt.eventId, syntheticReceipt.eventId); - } - - const preferSynthetic = ordering === null || ordering < 0; // we don't bother caching just real receipts by event ID as there's nothing that would read it. - // Take the current cached receipt before we overwrite the pair elements. - - const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; - - if (synthetic && preferSynthetic) { - pair[ReceiptPairSyntheticIndex] = wrappedReceipt; - } else if (!synthetic) { - pair[ReceiptPairRealIndex] = wrappedReceipt; - - if (!preferSynthetic) { - pair[ReceiptPairSyntheticIndex] = null; - } - } - - const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; - if (cachedReceipt === newCachedReceipt) return; // clean up any previous cache entry - - if (cachedReceipt && this.receiptCacheByEventId[cachedReceipt.eventId]) { - const previousEventId = cachedReceipt.eventId; // Remove the receipt we're about to clobber out of existence from the cache - - this.receiptCacheByEventId[previousEventId] = this.receiptCacheByEventId[previousEventId].filter(r => { - return r.type !== receiptType || r.userId !== userId; - }); - - if (this.receiptCacheByEventId[previousEventId].length < 1) { - delete this.receiptCacheByEventId[previousEventId]; // clean up the cache keys - } - } // cache the new one - - - if (!this.receiptCacheByEventId[eventId]) { - this.receiptCacheByEventId[eventId] = []; - } - - this.receiptCacheByEventId[eventId].push({ - userId: userId, - type: receiptType, - data: receipt - }); - }); - }); - }); - } - /** - * Add a temporary local-echo receipt to the room to reflect in the - * client the fact that we've sent one. - * @param {string} userId The user ID if the receipt sender - * @param {MatrixEvent} e The event that is to be acknowledged - * @param {ReceiptType} receiptType The type of receipt - */ - - - addLocalEchoReceipt(userId, e, receiptType) { - this.addReceipt(synthesizeReceipt(userId, e, receiptType), true); - } /** * Update the room-tag event for the room. The previous one is overwritten. - * @param {MatrixEvent} event the m.tag event + * @param event - the m.tag event */ - - addTags(event) { // event content looks like: // content: { @@ -2720,194 +2426,188 @@ // $tagName: { $metadata: $value }, // } // } + // XXX: do we need to deep copy here? - this.tags = event.getContent().tags || {}; // XXX: we could do a deep-comparison to see if the tags have really - // changed - but do we want to bother? + this.tags = event.getContent().tags || {}; + // XXX: we could do a deep-comparison to see if the tags have really + // changed - but do we want to bother? this.emit(RoomEvent.Tags, event, this); } + /** * Update the account_data events for this room, overwriting events of the same type. - * @param {Array} events an array of account_data events to add + * @param events - an array of account_data events to add */ - - addAccountData(events) { - for (let i = 0; i < events.length; i++) { - const event = events[i]; - + for (const event of events) { if (event.getType() === "m.tag") { this.addTags(event); } - - const lastEvent = this.accountData[event.getType()]; - this.accountData[event.getType()] = event; + const eventType = event.getType(); + const lastEvent = this.accountData.get(eventType); + this.accountData.set(eventType, event); this.emit(RoomEvent.AccountData, event, this, lastEvent); } } + /** * Access account_data event of given event type for this room - * @param {string} type the type of account_data event to be accessed - * @return {?MatrixEvent} the account_data event in question + * @param type - the type of account_data event to be accessed + * @returns the account_data event in question */ - - getAccountData(type) { - return this.accountData[type]; + return this.accountData.get(type); } + /** * Returns whether the syncing user has permission to send a message in the room - * @return {boolean} true if the user should be permitted to send + * @returns true if the user should be permitted to send * message events into the room. */ - - maySendMessage() { - return this.getMyMembership() === 'join' && (this.client.isRoomEncrypted(this.roomId) ? this.currentState.maySendEvent(_event2.EventType.RoomMessageEncrypted, this.myUserId) : this.currentState.maySendEvent(_event2.EventType.RoomMessage, this.myUserId)); + return this.getMyMembership() === "join" && (this.client.isRoomEncrypted(this.roomId) ? this.currentState.maySendEvent(_event2.EventType.RoomMessageEncrypted, this.myUserId) : this.currentState.maySendEvent(_event2.EventType.RoomMessage, this.myUserId)); } + /** * Returns whether the given user has permissions to issue an invite for this room. - * @param {string} userId the ID of the Matrix user to check permissions for - * @returns {boolean} true if the user should be permitted to issue invites for this room. + * @param userId - the ID of the Matrix user to check permissions for + * @returns true if the user should be permitted to issue invites for this room. */ - - canInvite(userId) { let canInvite = this.getMyMembership() === "join"; const powerLevelsEvent = this.currentState.getStateEvents(_event2.EventType.RoomPowerLevels, ""); const powerLevels = powerLevelsEvent && powerLevelsEvent.getContent(); const me = this.getMember(userId); - if (powerLevels && me && powerLevels.invite > me.powerLevel) { canInvite = false; } - return canInvite; } + /** * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`. - * @returns {string} the join_rule applied to this room + * @returns the join_rule applied to this room */ - - getJoinRule() { return this.currentState.getJoinRule(); } + /** * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns {HistoryVisibility} the history_visibility applied to this room + * @returns the history_visibility applied to this room */ - - getHistoryVisibility() { return this.currentState.getHistoryVisibility(); } + /** * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns {HistoryVisibility} the history_visibility applied to this room + * @returns the history_visibility applied to this room */ - - getGuestAccess() { return this.currentState.getGuestAccess(); } + /** * Returns the type of the room from the `m.room.create` event content or undefined if none is set - * @returns {?string} the type of the room. + * @returns the type of the room. */ - - getType() { const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); - if (!createEvent) { if (!this.getTypeWarning) { _logger.logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event"); - this.getTypeWarning = true; } - return undefined; } - return createEvent.getContent()[_event2.RoomCreateTypeField]; } + /** * Returns whether the room is a space-room as defined by MSC1772. - * @returns {boolean} true if the room's type is RoomType.Space + * @returns true if the room's type is RoomType.Space */ - - isSpaceRoom() { return this.getType() === _event2.RoomType.Space; } + /** * Returns whether the room is a call-room as defined by MSC3417. - * @returns {boolean} true if the room's type is RoomType.UnstableCall + * @returns true if the room's type is RoomType.UnstableCall */ - - isCallRoom() { return this.getType() === _event2.RoomType.UnstableCall; } + /** * Returns whether the room is a video room. - * @returns {boolean} true if the room's type is RoomType.ElementVideo + * @returns true if the room's type is RoomType.ElementVideo */ - - isElementVideoRoom() { return this.getType() === _event2.RoomType.ElementVideo; } + /** + * Find the predecessor of this room. + * + * @param msc3946ProcessDynamicPredecessor - if true, look for an + * m.room.predecessor state event and use it if found (MSC3946). + * @returns null if this room has no predecessor. Otherwise, returns + * the roomId and last eventId of the predecessor room. + * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events + * as well as m.room.create events to find predecessors. + * Note: if an m.predecessor event is used, eventId may be undefined + * since last_known_event_id is optional. + */ + findPredecessor(msc3946ProcessDynamicPredecessor = false) { + const currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); + if (!currentState) { + return null; + } + return currentState.findPredecessor(msc3946ProcessDynamicPredecessor); + } roomNameGenerator(state) { if (this.client.roomNameGenerator) { const name = this.client.roomNameGenerator(this.roomId, state); - if (name !== null) { return name; } } - switch (state.type) { case RoomNameType.Actual: return state.name; - case RoomNameType.Generated: switch (state.subtype) { case "Inviting": return `Inviting ${memberNamesToRoomName(state.names, state.count)}`; - default: return memberNamesToRoomName(state.names, state.count); } - case RoomNameType.EmptyRoom: if (state.oldName) { return `Empty room (was ${state.oldName})`; } else { return "Empty room"; } - } } + /** * This is an internal method. Calculates the name of the room from the current * room state. - * @param {string} userId The client's user ID. Used to filter room members + * @param userId - The client's user ID. Used to filter room members * correctly. - * @param {boolean} ignoreRoomNameEvent Return the implicit room name that we'd see if there + * @param ignoreRoomNameEvent - Return the implicit room name that we'd see if there * was no m.room.name event. - * @return {string} The calculated room name. + * @returns The calculated room name. */ - - calculateRoomName(userId, ignoreRoomNameEvent = false) { if (!ignoreRoomNameEvent) { // check for an alias, if any. for now, assume first alias is the // official one. const mRoomName = this.currentState.getStateEvents(_event2.EventType.RoomName, ""); - if (mRoomName?.getContent().name) { return this.roomNameGenerator({ type: RoomNameType.Actual, @@ -2915,42 +2615,35 @@ }); } } - const alias = this.getCanonicalAlias(); - if (alias) { return this.roomNameGenerator({ type: RoomNameType.Actual, name: alias }); } - const joinedMemberCount = this.currentState.getJoinedMemberCount(); - const invitedMemberCount = this.currentState.getInvitedMemberCount(); // -1 because these numbers include the syncing user - - let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1; // get service members (e.g. helper bots) for exclusion + const invitedMemberCount = this.currentState.getInvitedMemberCount(); + // -1 because these numbers include the syncing user + let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1; + // get service members (e.g. helper bots) for exclusion let excludedUserIds = []; const mFunctionalMembers = this.currentState.getStateEvents(_event2.UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, ""); - if (Array.isArray(mFunctionalMembers?.getContent().service_members)) { excludedUserIds = mFunctionalMembers.getContent().service_members; - } // get members that are NOT ourselves and are actually in the room. - - - let otherNames = null; + } + // get members that are NOT ourselves and are actually in the room. + let otherNames = []; if (this.summaryHeroes) { - // if we have a summary, the member state events - // should be in the room state - otherNames = []; + // if we have a summary, the member state events should be in the room state this.summaryHeroes.forEach(userId => { // filter service members if (excludedUserIds.includes(userId)) { inviteJoinCount--; return; } - const member = this.getMember(userId); otherNames.push(member ? member.name : userId); }); @@ -2966,16 +2659,14 @@ inviteJoinCount--; return false; } - return true; - }); // make sure members have stable order - - otherMembers.sort((a, b) => utils.compare(a.userId, b.userId)); // only 5 first members, immitate summaryHeroes - + }); + // make sure members have stable order + otherMembers.sort((a, b) => utils.compare(a.userId, b.userId)); + // only 5 first members, immitate summaryHeroes otherMembers = otherMembers.slice(0, 5); otherNames = otherMembers.map(m => m.name); } - if (inviteJoinCount) { return this.roomNameGenerator({ type: RoomNameType.Generated, @@ -2983,13 +2674,11 @@ count: inviteJoinCount }); } - - const myMembership = this.getMyMembership(); // if I have created a room and invited people through + const myMembership = this.getMyMembership(); + // if I have created a room and invited people through // 3rd party invites - - if (myMembership == 'join') { + if (myMembership == "join") { const thirdPartyInvites = this.currentState.getStateEvents(_event2.EventType.RoomThirdPartyInvite); - if (thirdPartyInvites?.length) { const thirdPartyNames = thirdPartyInvites.map(i => { return i.getContent().display_name; @@ -3001,19 +2690,17 @@ count: thirdPartyNames.length + 1 }); } - } // let's try to figure out who was here before - - - let leftNames = otherNames; // if we didn't have heroes, try finding them in the room state + } + // let's try to figure out who was here before + let leftNames = otherNames; + // if we didn't have heroes, try finding them in the room state if (!leftNames.length) { leftNames = this.currentState.getMembers().filter(m => { return m.userId !== userId && m.membership !== "invite" && m.membership !== "join"; }).map(m => m.name); } - let oldName; - if (leftNames.length) { oldName = this.roomNameGenerator({ type: RoomNameType.Generated, @@ -3021,12 +2708,12 @@ count: leftNames.length + 1 }); } - return this.roomNameGenerator({ type: RoomNameType.EmptyRoom, oldName }); } + /** * When we receive a new visibility change event: * @@ -3035,35 +2722,29 @@ * - if we have already received the event whose visibility has changed, * patch it to reflect the visibility change and inform listeners. */ - - applyNewVisibilityEvent(event) { const visibilityChange = event.asVisibilityChange(); - if (!visibilityChange) { // The event is ill-formed. return; - } // Ignore visibility change events that are not emitted by moderators. - + } + // Ignore visibility change events that are not emitted by moderators. const userId = event.getSender(); - if (!userId) { return; } - const isPowerSufficient = _event2.EVENT_VISIBILITY_CHANGE_TYPE.name && this.currentState.maySendStateEvent(_event2.EVENT_VISIBILITY_CHANGE_TYPE.name, userId) || _event2.EVENT_VISIBILITY_CHANGE_TYPE.altName && this.currentState.maySendStateEvent(_event2.EVENT_VISIBILITY_CHANGE_TYPE.altName, userId); - if (!isPowerSufficient) { // Powerlevel is insufficient. return; - } // Record this change in visibility. + } + + // Record this change in visibility. // If the event is not in our timeline and we only receive it later, // we may need to apply the visibility change at a later date. - const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId); - if (visibilityEventsOnOriginalEvent) { // It would be tempting to simply erase the latest visibility change // but we need to record all of the changes in case the latest change @@ -3074,15 +2755,12 @@ // number of iterations in this loop. let index = visibilityEventsOnOriginalEvent.length - 1; const min = Math.max(0, visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH); - for (; index >= min; --index) { const target = visibilityEventsOnOriginalEvent[index]; - if (target.getTs() < event.getTs()) { break; } } - if (index === -1) { visibilityEventsOnOriginalEvent.unshift(event); } else { @@ -3090,54 +2768,46 @@ } } else { this.visibilityEvents.set(visibilityChange.eventId, [event]); - } // Finally, let's check if the event is already in our timeline. - // If so, we need to patch it and inform listeners. + } + // Finally, let's check if the event is already in our timeline. + // If so, we need to patch it and inform listeners. const originalEvent = this.findEventById(visibilityChange.eventId); - if (!originalEvent) { return; } - originalEvent.applyVisibilityEvent(visibilityChange); } - redactVisibilityChangeEvent(event) { // Sanity checks. if (!event.isVisibilityEvent) { throw new Error("expected a visibility change event"); } - const relation = event.getRelation(); - const originalEventId = relation.event_id; + const originalEventId = relation?.event_id; const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId); - if (!visibilityEventsOnOriginalEvent) { // No visibility changes on the original event. // In particular, this change event was not recorded, // most likely because it was ill-formed. return; } - const index = visibilityEventsOnOriginalEvent.findIndex(change => change.getId() === event.getId()); - if (index === -1) { // This change event was not recorded, most likely because // it was ill-formed. return; - } // Remove visibility change. - - - visibilityEventsOnOriginalEvent.splice(index, 1); // If we removed the latest visibility change event, propagate changes. + } + // Remove visibility change. + visibilityEventsOnOriginalEvent.splice(index, 1); + // If we removed the latest visibility change event, propagate changes. if (index === visibilityEventsOnOriginalEvent.length) { const originalEvent = this.findEventById(originalEventId); - if (!originalEvent) { return; } - if (index === 0) { // We have just removed the only visibility change event. this.visibilityEvents.delete(originalEventId); @@ -3145,83 +2815,110 @@ } else { const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1]; const newVisibility = newEvent.asVisibilityChange(); - if (!newVisibility) { // Event is ill-formed. // This breaks our invariant. throw new Error("at this stage, visibility changes should be well-formed"); } - originalEvent.applyVisibilityEvent(newVisibility); } } } + /** * When we receive an event whose visibility has been altered by * a (more recent) visibility change event, patch the event in * place so that clients now not to display it. * - * @param event Any matrix event. If this event has at least one a + * @param event - Any matrix event. If this event has at least one a * pending visibility change event, apply the latest visibility * change event. */ - - applyPendingVisibilityEvents(event) { const visibilityEvents = this.visibilityEvents.get(event.getId()); - if (!visibilityEvents || visibilityEvents.length == 0) { // No pending visibility change in store. return; } - const visibilityEvent = visibilityEvents[visibilityEvents.length - 1]; const visibilityChange = visibilityEvent.asVisibilityChange(); - if (!visibilityChange) { return; } - - if (visibilityChange.visible) {// Events are visible by default, no need to apply a visibility change. + if (visibilityChange.visible) { + // Events are visible by default, no need to apply a visibility change. // Note that we need to keep the visibility changes in `visibilityEvents`, // in case we later fetch an older visibility change event that is superseded // by `visibilityChange`. } - if (visibilityEvent.getTs() < event.getTs()) { // Something is wrong, the visibility change cannot happen before the // event. Presumably an ill-formed event. return; } - event.applyVisibilityEvent(visibilityChange); } -} // a map from current event status to a list of allowed next statuses + /** + * Find when a client has gained thread capabilities by inspecting the oldest + * threaded receipt + * @returns the timestamp of the oldest threaded receipt + */ + getOldestThreadedReceiptTs() { + return this.oldestThreadedReceiptTs; + } + + /** + * Returns the most recent unthreaded receipt for a given user + * @param userId - the MxID of the User + * @returns an unthreaded Receipt. Can be undefined if receipts have been disabled + * or a user chooses to use private read receipts (or we have simply not received + * a receipt from this user yet). + */ + getLastUnthreadedReceiptFor(userId) { + return this.unthreadedReceipts.get(userId); + } + /** + * This issue should also be addressed on synapse's side and is tracked as part + * of https://github.com/matrix-org/synapse/issues/14837 + * + * + * We consider a room fully read if the current user has sent + * the last event in the live timeline of that context and if the read receipt + * we have on record matches. + * This also detects all unread threads and applies the same logic to those + * contexts + */ + fixupNotifications(userId) { + super.fixupNotifications(userId); + const unreadThreads = this.getThreads().filter(thread => this.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total) > 0); + for (const thread of unreadThreads) { + thread.fixupNotifications(userId); + } + } +} +// a map from current event status to a list of allowed next statuses exports.Room = Room; const ALLOWED_TRANSITIONS = { [_eventStatus.EventStatus.ENCRYPTING]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.NOT_SENT, _eventStatus.EventStatus.CANCELLED], [_eventStatus.EventStatus.SENDING]: [_eventStatus.EventStatus.ENCRYPTING, _eventStatus.EventStatus.QUEUED, _eventStatus.EventStatus.NOT_SENT, _eventStatus.EventStatus.SENT], - [_eventStatus.EventStatus.QUEUED]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.CANCELLED], + [_eventStatus.EventStatus.QUEUED]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.NOT_SENT, _eventStatus.EventStatus.CANCELLED], [_eventStatus.EventStatus.SENT]: [], [_eventStatus.EventStatus.NOT_SENT]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.QUEUED, _eventStatus.EventStatus.CANCELLED], [_eventStatus.EventStatus.CANCELLED]: [] }; let RoomNameType; exports.RoomNameType = RoomNameType; - (function (RoomNameType) { RoomNameType[RoomNameType["EmptyRoom"] = 0] = "EmptyRoom"; RoomNameType[RoomNameType["Generated"] = 1] = "Generated"; RoomNameType[RoomNameType["Actual"] = 2] = "Actual"; })(RoomNameType || (exports.RoomNameType = RoomNameType = {})); - // Can be overriden by IMatrixClientCreateOpts::memberNamesToRoomNameFn function memberNamesToRoomName(names, count) { const countWithoutMe = count - 1; - if (!names.length) { return "Empty room"; } else if (names.length === 1 && countWithoutMe <= 1) { @@ -3230,124 +2927,10 @@ return `${names[0]} and ${names[1]}`; } else { const plural = countWithoutMe > 1; - if (plural) { return `${names[0]} and ${countWithoutMe} others`; } else { return `${names[0]} and 1 other`; } } -} -/** - * Fires when an event we had previously received is redacted. - * - * (Note this is *not* fired when the redaction happens before we receive the - * event). - * - * @event module:client~MatrixClient#"Room.redaction" - * @param {MatrixEvent} event The matrix redaction event - * @param {Room} room The room containing the redacted event - */ - -/** - * Fires when an event that was previously redacted isn't anymore. - * This happens when the redaction couldn't be sent and - * was subsequently cancelled by the user. Redactions have a local echo - * which is undone in this scenario. - * - * @event module:client~MatrixClient#"Room.redactionCancelled" - * @param {MatrixEvent} event The matrix redaction event that was cancelled. - * @param {Room} room The room containing the unredacted event - */ - -/** - * Fires whenever the name of a room is updated. - * @event module:client~MatrixClient#"Room.name" - * @param {Room} room The room whose Room.name was updated. - * @example - * matrixClient.on("Room.name", function(room){ - * var newName = room.name; - * }); - */ - -/** - * Fires whenever a receipt is received for a room - * @event module:client~MatrixClient#"Room.receipt" - * @param {event} event The receipt event - * @param {Room} room The room whose receipts was updated. - * @example - * matrixClient.on("Room.receipt", function(event, room){ - * var receiptContent = event.getContent(); - * }); - */ - -/** - * Fires whenever a room's tags are updated. - * @event module:client~MatrixClient#"Room.tags" - * @param {event} event The tags event - * @param {Room} room The room whose Room.tags was updated. - * @example - * matrixClient.on("Room.tags", function(event, room){ - * var newTags = event.getContent().tags; - * if (newTags["favourite"]) showStar(room); - * }); - */ - -/** - * Fires whenever a room's account_data is updated. - * @event module:client~MatrixClient#"Room.accountData" - * @param {event} event The account_data event - * @param {Room} room The room whose account_data was updated. - * @param {MatrixEvent} prevEvent The event being replaced by - * the new account data, if known. - * @example - * matrixClient.on("Room.accountData", function(event, room, oldEvent){ - * if (event.getType() === "m.room.colorscheme") { - * applyColorScheme(event.getContents()); - * } - * }); - */ - -/** - * Fires when the status of a transmitted event is updated. - * - *

When an event is first transmitted, a temporary copy of the event is - * inserted into the timeline, with a temporary event id, and a status of - * 'SENDING'. - * - *

Once the echo comes back from the server, the content of the event - * (MatrixEvent.event) is replaced by the complete event from the homeserver, - * thus updating its event id, as well as server-generated fields such as the - * timestamp. Its status is set to null. - * - *

Once the /send request completes, if the remote echo has not already - * arrived, the event is updated with a new event id and the status is set to - * 'SENT'. The server-generated fields are of course not updated yet. - * - *

If the /send fails, In this case, the event's status is set to - * 'NOT_SENT'. If it is later resent, the process starts again, setting the - * status to 'SENDING'. Alternatively, the message may be cancelled, which - * removes the event from the room, and sets the status to 'CANCELLED'. - * - *

This event is raised to reflect each of the transitions above. - * - * @event module:client~MatrixClient#"Room.localEchoUpdated" - * - * @param {MatrixEvent} event The matrix event which has been updated - * - * @param {Room} room The room containing the redacted event - * - * @param {string} oldEventId The previous event id (the temporary event id, - * except when updating a successfully-sent event when its echo arrives) - * - * @param {EventStatus} oldStatus The previous event status. - */ - -/** - * Fires when the logged in user's membership in the room is updated. - * - * @event module:models/room~Room#"Room.myMembership" - * @param {Room} room The room in which the membership has been updated - * @param {string} membership The new membership value - * @param {string} prevMembership The previous membership value - */ \ No newline at end of file +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-member.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-member.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-member.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-member.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,187 +4,174 @@ value: true }); exports.RoomMemberEvent = exports.RoomMember = void 0; - var _contentRepo = require("../content-repo"); - var utils = _interopRequireWildcard(require("../utils")); - var _logger = require("../logger"); - var _typedEventEmitter = require("./typed-event-emitter"); - var _event = require("../@types/event"); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } let RoomMemberEvent; exports.RoomMemberEvent = RoomMemberEvent; - (function (RoomMemberEvent) { RoomMemberEvent["Membership"] = "RoomMember.membership"; RoomMemberEvent["Name"] = "RoomMember.name"; RoomMemberEvent["PowerLevel"] = "RoomMember.powerLevel"; RoomMemberEvent["Typing"] = "RoomMember.typing"; })(RoomMemberEvent || (exports.RoomMemberEvent = RoomMemberEvent = {})); - class RoomMember extends _typedEventEmitter.TypedEventEmitter { // used by sync.ts + // XXX these should be read-only + /** + * True if the room member is currently typing. + */ + + /** + * The human-readable name for this room member. This will be + * disambiguated with a suffix of " (\@user_id:matrix.org)" if another member shares the + * same displayname. + */ + + /** + * The ambiguous displayname of this room member. + */ + + /** + * The power level for this room member. + */ + + /** + * The normalised power level (0-100) for this room member. + */ + + /** + * The User object for this room member, if one exists. + */ + + /** + * The membership state for this room member e.g. 'join'. + */ + + /** + * True if the member's name is disambiguated. + */ + + /** + * The events describing this RoomMember. + */ /** * Construct a new room member. * - * @constructor - * @alias module:models/room-member - * - * @param {string} roomId The room ID of the member. - * @param {string} userId The user ID of the member. - * @prop {string} roomId The room ID for this member. - * @prop {string} userId The user ID of this member. - * @prop {boolean} typing True if the room member is currently typing. - * @prop {string} name The human-readable name for this room member. This will be - * disambiguated with a suffix of " (@user_id:matrix.org)" if another member shares the - * same displayname. - * @prop {string} rawDisplayName The ambiguous displayname of this room member. - * @prop {Number} powerLevel The power level for this room member. - * @prop {Number} powerLevelNorm The normalised power level (0-100) for this - * room member. - * @prop {User} user The User object for this room member, if one exists. - * @prop {string} membership The membership state for this room member e.g. 'join'. - * @prop {Object} events The events describing this RoomMember. - * @prop {MatrixEvent} events.member The m.room.member event for this RoomMember. - * @prop {boolean} disambiguate True if the member's name is disambiguated. + * @param roomId - The room ID of the member. + * @param userId - The user ID of the member. */ constructor(roomId, userId) { super(); this.roomId = roomId; this.userId = userId; - _defineProperty(this, "_isOutOfBand", false); - - _defineProperty(this, "_modified", void 0); - - _defineProperty(this, "_requestedProfileInfo", void 0); - + _defineProperty(this, "modified", -1); + _defineProperty(this, "requestedProfileInfo", false); _defineProperty(this, "typing", false); - _defineProperty(this, "name", void 0); - _defineProperty(this, "rawDisplayName", void 0); - _defineProperty(this, "powerLevel", 0); - _defineProperty(this, "powerLevelNorm", 0); - - _defineProperty(this, "user", null); - - _defineProperty(this, "membership", null); - + _defineProperty(this, "user", void 0); + _defineProperty(this, "membership", void 0); _defineProperty(this, "disambiguate", false); - - _defineProperty(this, "events", { - member: null - }); - + _defineProperty(this, "events", {}); this.name = userId; this.rawDisplayName = userId; this.updateModifiedTime(); } + /** * Mark the member as coming from a channel that is not sync */ - - markOutOfBand() { this._isOutOfBand = true; } + /** - * @return {boolean} does the member come from a channel that is not sync? + * @returns does the member come from a channel that is not sync? * This is used to store the member seperately * from the sync state so it available across browser sessions. */ - - isOutOfBand() { return this._isOutOfBand; } + /** * Update this room member's membership event. May fire "RoomMember.name" if * this event updates this member's name. - * @param {MatrixEvent} event The m.room.member event - * @param {RoomState} roomState Optional. The room state to take into account + * @param event - The `m.room.member` event + * @param roomState - Optional. The room state to take into account * when calculating (e.g. for disambiguating users with the same name). - * @fires module:client~MatrixClient#event:"RoomMember.name" - * @fires module:client~MatrixClient#event:"RoomMember.membership" + * + * @remarks + * Fires {@link RoomMemberEvent.Name} + * Fires {@link RoomMemberEvent.Membership} */ - - setMembershipEvent(event, roomState) { - const displayName = event.getDirectionalContent().displayname; - + const displayName = event.getDirectionalContent().displayname ?? ""; if (event.getType() !== _event.EventType.RoomMember) { return; } - this._isOutOfBand = false; this.events.member = event; const oldMembership = this.membership; this.membership = event.getDirectionalContent().membership; - if (this.membership === undefined) { // logging to diagnose https://github.com/vector-im/element-web/issues/20962 // (logs event content, although only of membership events) _logger.logger.trace(`membership event with membership undefined (forwardLooking: ${event.forwardLooking})!`, event.getContent(), `prevcontent is `, event.getPrevContent()); } - this.disambiguate = shouldDisambiguate(this.userId, displayName, roomState); const oldName = this.name; - this.name = calculateDisplayName(this.userId, displayName, roomState, this.disambiguate); // not quite raw: we strip direction override chars so it can safely be inserted into - // blocks of text without breaking the text direction - - this.rawDisplayName = utils.removeDirectionOverrideChars(event.getDirectionalContent().displayname); + this.name = calculateDisplayName(this.userId, displayName, this.disambiguate); + // not quite raw: we strip direction override chars so it can safely be inserted into + // blocks of text without breaking the text direction + this.rawDisplayName = utils.removeDirectionOverrideChars(event.getDirectionalContent().displayname ?? ""); if (!this.rawDisplayName || !utils.removeHiddenChars(this.rawDisplayName)) { this.rawDisplayName = this.userId; } - if (oldMembership !== this.membership) { this.updateModifiedTime(); this.emit(RoomMemberEvent.Membership, event, this, oldMembership); } - if (oldName !== this.name) { this.updateModifiedTime(); this.emit(RoomMemberEvent.Name, event, this, oldName); } } + /** * Update this room member's power level event. May fire * "RoomMember.powerLevel" if this event updates this member's power levels. - * @param {MatrixEvent} powerLevelEvent The m.room.power_levels - * event - * @fires module:client~MatrixClient#event:"RoomMember.powerLevel" + * @param powerLevelEvent - The `m.room.power_levels` event + * + * @remarks + * Fires {@link RoomMemberEvent.PowerLevel} */ - - setPowerLevelEvent(powerLevelEvent) { - if (powerLevelEvent.getType() !== "m.room.power_levels") { + if (powerLevelEvent.getType() !== _event.EventType.RoomPowerLevels || powerLevelEvent.getStateKey() !== "") { return; } - const evContent = powerLevelEvent.getDirectionalContent(); let maxLevel = evContent.users_default || 0; const users = evContent.users || {}; - Object.values(users).forEach(function (lvl) { + Object.values(users).forEach(lvl => { maxLevel = Math.max(maxLevel, lvl); }); const oldPowerLevel = this.powerLevel; const oldPowerLevelNorm = this.powerLevelNorm; - if (users[this.userId] !== undefined && Number.isInteger(users[this.userId])) { this.powerLevel = users[this.userId]; } else if (evContent.users_default !== undefined) { @@ -192,188 +179,174 @@ } else { this.powerLevel = 0; } - this.powerLevelNorm = 0; - if (maxLevel > 0) { this.powerLevelNorm = this.powerLevel * 100 / maxLevel; - } // emit for changes in powerLevelNorm as well (since the app will need to - // redraw everyone's level if the max has changed) - + } + // emit for changes in powerLevelNorm as well (since the app will need to + // redraw everyone's level if the max has changed) if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) { this.updateModifiedTime(); this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this); } } + /** * Update this room member's typing event. May fire "RoomMember.typing" if * this event changes this member's typing state. - * @param {MatrixEvent} event The typing event - * @fires module:client~MatrixClient#event:"RoomMember.typing" + * @param event - The typing event + * + * @remarks + * Fires {@link RoomMemberEvent.Typing} */ - - setTypingEvent(event) { if (event.getType() !== "m.typing") { return; } - const oldTyping = this.typing; this.typing = false; const typingList = event.getContent().user_ids; - if (!Array.isArray(typingList)) { // malformed event :/ bail early. TODO: whine? return; } - if (typingList.indexOf(this.userId) !== -1) { this.typing = true; } - if (oldTyping !== this.typing) { this.updateModifiedTime(); this.emit(RoomMemberEvent.Typing, event, this); } } + /** * Update the last modified time to the current time. */ - - updateModifiedTime() { - this._modified = Date.now(); + this.modified = Date.now(); } + /** * Get the timestamp when this RoomMember was last updated. This timestamp is * updated when properties on this RoomMember are updated. * It is updated before firing events. - * @return {number} The timestamp + * @returns The timestamp */ - - getLastModifiedTime() { - return this._modified; + return this.modified; } - isKicked() { - return this.membership === "leave" && this.events.member.getSender() !== this.events.member.getStateKey(); + return this.membership === "leave" && this.events.member !== undefined && this.events.member.getSender() !== this.events.member.getStateKey(); } + /** * If this member was invited with the is_direct flag set, return * the user that invited this member - * @return {string} user id of the inviter + * @returns user id of the inviter */ - - getDMInviter() { // when not available because that room state hasn't been loaded in, // we don't really know, but more likely to not be a direct chat if (this.events.member) { // TODO: persist the is_direct flag on the member as more member events // come in caused by displayName changes. + // the is_direct flag is set on the invite member event. // This is copied on the prev_content section of the join member event // when the invite is accepted. + const memberEvent = this.events.member; let memberContent = memberEvent.getContent(); let inviteSender = memberEvent.getSender(); - if (memberContent.membership === "join") { memberContent = memberEvent.getPrevContent(); inviteSender = memberEvent.getUnsigned().prev_sender; } - if (memberContent.membership === "invite" && memberContent.is_direct) { return inviteSender; } } } + /** * Get the avatar URL for a room member. - * @param {string} baseUrl The base homeserver URL See - * {@link module:client~MatrixClient#getHomeserverUrl}. - * @param {Number} width The desired width of the thumbnail. - * @param {Number} height The desired height of the thumbnail. - * @param {string} resizeMethod The thumbnail resize method to use, either + * @param baseUrl - The base homeserver URL See + * {@link MatrixClient#getHomeserverUrl}. + * @param width - The desired width of the thumbnail. + * @param height - The desired height of the thumbnail. + * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". - * @param {Boolean} allowDefault (optional) Passing false causes this method to + * @param allowDefault - (optional) Passing false causes this method to * return null if the user has no avatar image. Otherwise, a default image URL * will be returned. Default: true. (Deprecated) - * @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be + * @param allowDirectLinks - (optional) If true, the avatar URL will be * returned even if it is a direct hyperlink rather than a matrix content URL. * If false, any non-matrix content URLs will be ignored. Setting this option to * true will expose URLs that, if fetched, will leak information about the user * to anyone who they share a room with. - * @return {?string} the avatar URL or null. + * @returns the avatar URL or null. */ - - getAvatarUrl(baseUrl, width, height, resizeMethod, allowDefault = true, allowDirectLinks) { const rawUrl = this.getMxcAvatarUrl(); - if (!rawUrl && !allowDefault) { return null; } - const httpUrl = (0, _contentRepo.getHttpUriForMxc)(baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks); - if (httpUrl) { return httpUrl; } - return null; } + /** * get the mxc avatar url, either from a state event, or from a lazily loaded member - * @return {string} the mxc avatar url + * @returns the mxc avatar url */ - - getMxcAvatarUrl() { if (this.events.member) { return this.events.member.getDirectionalContent().avatar_url; } else if (this.user) { return this.user.avatarUrl; } - - return null; } - } - exports.RoomMember = RoomMember; const MXID_PATTERN = /@.+:.+/; const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/; - function shouldDisambiguate(selfUserId, displayName, roomState) { - if (!displayName || displayName === selfUserId) return false; // First check if the displayname is something we consider truthy - // after stripping it of zero width characters and padding spaces + if (!displayName || displayName === selfUserId) return false; + // First check if the displayname is something we consider truthy + // after stripping it of zero width characters and padding spaces if (!utils.removeHiddenChars(displayName)) return false; - if (!roomState) return false; // Next check if the name contains something that look like a mxid + if (!roomState) return false; + + // Next check if the name contains something that look like a mxid // If it does, it may be someone trying to impersonate someone else // Show full mxid in this case + if (MXID_PATTERN.test(displayName)) return true; - if (MXID_PATTERN.test(displayName)) return true; // Also show mxid if the display name contains any LTR/RTL characters as these + // Also show mxid if the display name contains any LTR/RTL characters as these // make it very difficult for us to find similar *looking* display names // E.g "Mark" could be cloned by writing "kraM" but in RTL. + if (LTR_RTL_PATTERN.test(displayName)) return true; - if (LTR_RTL_PATTERN.test(displayName)) return true; // Also show mxid if there are other people with the same or similar + // Also show mxid if there are other people with the same or similar // displayname, after hidden character removal. - const userIds = roomState.getUserIdsWithDisplayName(displayName); if (userIds.some(u => u !== selfUserId)) return true; return false; } - -function calculateDisplayName(selfUserId, displayName, roomState, disambiguate) { +function calculateDisplayName(selfUserId, displayName, disambiguate) { + if (!displayName || displayName === selfUserId) return selfUserId; if (disambiguate) return utils.removeDirectionOverrideChars(displayName) + " (" + selfUserId + ")"; - if (!displayName || displayName === selfUserId) return selfUserId; // First check if the displayname is something we consider truthy + + // First check if the displayname is something we consider truthy // after stripping it of zero width characters and padding spaces + if (!utils.removeHiddenChars(displayName)) return selfUserId; - if (!utils.removeHiddenChars(displayName)) return selfUserId; // We always strip the direction override characters (LRO and RLO). + // We always strip the direction override characters (LRO and RLO). // These override the text direction for all subsequent characters // in the paragraph so if display names contained these, they'd // need to be wrapped in something to prevent this from leaking out @@ -384,54 +357,5 @@ // names should flip into the correct direction automatically based on // the characters, and you can still embed rtl in ltr or vice versa // with the embed chars or marker chars. - return utils.removeDirectionOverrideChars(displayName); -} -/** - * Fires whenever any room member's name changes. - * @event module:client~MatrixClient#"RoomMember.name" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {RoomMember} member The member whose RoomMember.name changed. - * @param {string?} oldName The previous name. Null if the member didn't have a - * name previously. - * @example - * matrixClient.on("RoomMember.name", function(event, member){ - * var newName = member.name; - * }); - */ - -/** - * Fires whenever any room member's membership state changes. - * @event module:client~MatrixClient#"RoomMember.membership" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {RoomMember} member The member whose RoomMember.membership changed. - * @param {string?} oldMembership The previous membership state. Null if it's a - * new member. - * @example - * matrixClient.on("RoomMember.membership", function(event, member, oldMembership){ - * var newState = member.membership; - * }); - */ - -/** - * Fires whenever any room member's typing state changes. - * @event module:client~MatrixClient#"RoomMember.typing" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {RoomMember} member The member whose RoomMember.typing changed. - * @example - * matrixClient.on("RoomMember.typing", function(event, member){ - * var isTyping = member.typing; - * }); - */ - -/** - * Fires whenever any room member's power level changes. - * @event module:client~MatrixClient#"RoomMember.powerLevel" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {RoomMember} member The member whose RoomMember.powerLevel changed. - * @example - * matrixClient.on("RoomMember.powerLevel", function(event, member){ - * var newPowerLevel = member.powerLevel; - * var newNormPowerLevel = member.powerLevelNorm; - * }); - */ \ No newline at end of file +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-state.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-state.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-state.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-state.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,49 +4,32 @@ value: true }); exports.RoomStateEvent = exports.RoomState = void 0; - var _roomMember = require("./room-member"); - var _logger = require("../logger"); - var utils = _interopRequireWildcard(require("../utils")); - var _event = require("../@types/event"); - var _event2 = require("./event"); - var _partials = require("../@types/partials"); - var _typedEventEmitter = require("./typed-event-emitter"); - var _beacon = require("./beacon"); - var _ReEmitter = require("../ReEmitter"); - var _beacon2 = require("../@types/beacon"); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // possible statuses for out-of-band member loading var OobStatus; - (function (OobStatus) { OobStatus[OobStatus["NotStarted"] = 0] = "NotStarted"; OobStatus[OobStatus["InProgress"] = 1] = "InProgress"; OobStatus[OobStatus["Finished"] = 2] = "Finished"; })(OobStatus || (OobStatus = {})); - let RoomStateEvent; exports.RoomStateEvent = RoomStateEvent; - (function (RoomStateEvent) { RoomStateEvent["Events"] = "RoomState.events"; RoomStateEvent["Members"] = "RoomState.members"; @@ -55,10 +38,10 @@ RoomStateEvent["BeaconLiveness"] = "RoomState.BeaconLiveness"; RoomStateEvent["Marker"] = "RoomState.Marker"; })(RoomStateEvent || (exports.RoomStateEvent = RoomStateEvent = {})); - class RoomState extends _typedEventEmitter.TypedEventEmitter { // userId: RoomMember // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys) + // 3pid invite state_key to m.room.member invite // cache of the number of joined members // joined members count from summary api @@ -66,10 +49,15 @@ // and we should only trust that // we could also only trust that before OOB members // are loaded but doesn't seem worth the hassle atm + // same for invited member count + // XXX: Should be read-only + // The room member dictionary, keyed on the user's ID. // userId: RoomMember + // The state events dictionary, keyed on the event type and then the state_key value. // Map> + // The pagination token for this state. /** * Construct room state. @@ -78,8 +66,8 @@ * It can be mutated by adding state events to it. * There are two types of room member associated with a state event: * normal member objects (accessed via getMember/getMembers) which mutate - * with the state to represent the current state of that room/user, eg. - * the object returned by getMember('@bob:example.com') will mutate to + * with the state to represent the current state of that room/user, e.g. + * the object returned by `getMember('@bob:example.com')` will mutate to * get a different display name if Bob later changes his display name * in the room. * There are also 'sentinel' members (accessed via getSentinelMember). @@ -92,18 +80,12 @@ * after the display name change will return a new RoomMember object * with Bob's new display name. * - * @constructor - * @param {?string} roomId Optional. The ID of the room which has this state. + * @param roomId - Optional. The ID of the room which has this state. * If none is specified it just tracks paginationTokens, useful for notifTimelineSet - * @param {?object} oobMemberFlags Optional. The state of loading out of bound members. + * @param oobMemberFlags - Optional. The state of loading out of bound members. * As the timeline might get reset while they are loading, this state needs to be inherited * and shared when the room state is cloned for the new timeline. * This should only be passed from clone. - * @prop {Object.} members The room member dictionary, keyed - * on the user's ID. - * @prop {Object.>} events The state - * events dictionary, keyed on the event type and then the state_key value. - * @prop {string} paginationToken The pagination token for this state. */ constructor(roomId, oobMemberFlags = { status: OobStatus.NotStarted @@ -111,309 +93,253 @@ super(); this.roomId = roomId; this.oobMemberFlags = oobMemberFlags; - _defineProperty(this, "reEmitter", new _ReEmitter.TypedReEmitter(this)); - _defineProperty(this, "sentinels", {}); - _defineProperty(this, "displayNameToUserIds", new Map()); - _defineProperty(this, "userIdsToDisplayNames", {}); - _defineProperty(this, "tokenToInvite", {}); - _defineProperty(this, "joinedMemberCount", null); - _defineProperty(this, "summaryJoinedMemberCount", null); - _defineProperty(this, "invitedMemberCount", null); - _defineProperty(this, "summaryInvitedMemberCount", null); - - _defineProperty(this, "modified", void 0); - + _defineProperty(this, "modified", -1); _defineProperty(this, "members", {}); - _defineProperty(this, "events", new Map()); - _defineProperty(this, "paginationToken", null); - _defineProperty(this, "beacons", new Map()); - _defineProperty(this, "_liveBeaconIds", []); - this.updateModifiedTime(); } + /** * Returns the number of joined members in this room * This method caches the result. - * @return {number} The number of members in this room whose membership is 'join' + * @returns The number of members in this room whose membership is 'join' */ - - getJoinedMemberCount() { if (this.summaryJoinedMemberCount !== null) { return this.summaryJoinedMemberCount; } - if (this.joinedMemberCount === null) { this.joinedMemberCount = this.getMembers().reduce((count, m) => { - return m.membership === 'join' ? count + 1 : count; + return m.membership === "join" ? count + 1 : count; }, 0); } - return this.joinedMemberCount; } + /** * Set the joined member count explicitly (like from summary part of the sync response) - * @param {number} count the amount of joined members + * @param count - the amount of joined members */ - - setJoinedMemberCount(count) { this.summaryJoinedMemberCount = count; } + /** * Returns the number of invited members in this room - * @return {number} The number of members in this room whose membership is 'invite' + * @returns The number of members in this room whose membership is 'invite' */ - - getInvitedMemberCount() { if (this.summaryInvitedMemberCount !== null) { return this.summaryInvitedMemberCount; } - if (this.invitedMemberCount === null) { this.invitedMemberCount = this.getMembers().reduce((count, m) => { - return m.membership === 'invite' ? count + 1 : count; + return m.membership === "invite" ? count + 1 : count; }, 0); } - return this.invitedMemberCount; } + /** * Set the amount of invited members in this room - * @param {number} count the amount of invited members + * @param count - the amount of invited members */ - - setInvitedMemberCount(count) { this.summaryInvitedMemberCount = count; } + /** * Get all RoomMembers in this room. - * @return {Array} A list of RoomMembers. + * @returns A list of RoomMembers. */ - - getMembers() { return Object.values(this.members); } + /** * Get all RoomMembers in this room, excluding the user IDs provided. - * @param {Array} excludedIds The user IDs to exclude. - * @return {Array} A list of RoomMembers. + * @param excludedIds - The user IDs to exclude. + * @returns A list of RoomMembers. */ - - getMembersExcept(excludedIds) { return this.getMembers().filter(m => !excludedIds.includes(m.userId)); } + /** * Get a room member by their user ID. - * @param {string} userId The room member's user ID. - * @return {RoomMember} The member or null if they do not exist. + * @param userId - The room member's user ID. + * @returns The member or null if they do not exist. */ - - getMember(userId) { return this.members[userId] || null; } + /** * Get a room member whose properties will not change with this room state. You * typically want this if you want to attach a RoomMember to a MatrixEvent which * may no longer be represented correctly by Room.currentState or Room.oldState. * The term 'sentinel' refers to the fact that this RoomMember is an unchanging * guardian for state at this particular point in time. - * @param {string} userId The room member's user ID. - * @return {RoomMember} The member or null if they do not exist. + * @param userId - The room member's user ID. + * @returns The member or null if they do not exist. */ - - getSentinelMember(userId) { if (!userId) return null; let sentinel = this.sentinels[userId]; - if (sentinel === undefined) { sentinel = new _roomMember.RoomMember(this.roomId, userId); const member = this.members[userId]; - - if (member) { + if (member?.events.member) { sentinel.setMembershipEvent(member.events.member, this); } - this.sentinels[userId] = sentinel; } - return sentinel; } + /** * Get state events from the state of the room. - * @param {string} eventType The event type of the state event. - * @param {string} stateKey Optional. The state_key of the state event. If - * this is undefined then all matching state events will be + * @param eventType - The event type of the state event. + * @param stateKey - Optional. The state_key of the state event. If + * this is `undefined` then all matching state events will be * returned. - * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was - * undefined, else a single event (or null if no match found). + * @returns A list of events if state_key was + * `undefined`, else a single event (or null if no match found). */ - getStateEvents(eventType, stateKey) { if (!this.events.has(eventType)) { // no match return stateKey === undefined ? [] : null; } - if (stateKey === undefined) { // return all values return Array.from(this.events.get(eventType).values()); } - const event = this.events.get(eventType).get(stateKey); return event ? event : null; } - get hasLiveBeacons() { return !!this.liveBeaconIds?.length; } - get liveBeaconIds() { return this._liveBeaconIds; } + /** * Creates a copy of this room state so that mutations to either won't affect the other. - * @return {RoomState} the copy of the room state + * @returns the copy of the room state */ - - clone() { - const copy = new RoomState(this.roomId, this.oobMemberFlags); // Ugly hack: because setStateEvents will mark + const copy = new RoomState(this.roomId, this.oobMemberFlags); + + // Ugly hack: because setStateEvents will mark // members as susperseding future out of bound members // if loading is in progress (through oobMemberFlags) // since these are not new members, we're merely copying them // set the status to not started // after copying, we set back the status - const status = this.oobMemberFlags.status; this.oobMemberFlags.status = OobStatus.NotStarted; Array.from(this.events.values()).forEach(eventsByStateKey => { copy.setStateEvents(Array.from(eventsByStateKey.values())); - }); // Ugly hack: see above + }); + // Ugly hack: see above this.oobMemberFlags.status = status; - if (this.summaryInvitedMemberCount !== null) { copy.setInvitedMemberCount(this.getInvitedMemberCount()); } - if (this.summaryJoinedMemberCount !== null) { copy.setJoinedMemberCount(this.getJoinedMemberCount()); - } // copy out of band flags if needed - + } + // copy out of band flags if needed if (this.oobMemberFlags.status == OobStatus.Finished) { // copy markOutOfBand flags this.getMembers().forEach(member => { if (member.isOutOfBand()) { - const copyMember = copy.getMember(member.userId); - copyMember.markOutOfBand(); + copy.getMember(member.userId)?.markOutOfBand(); } }); } - return copy; } + /** * Add previously unknown state events. * When lazy loading members while back-paginating, * the relevant room state for the timeline chunk at the end * of the chunk can be set with this method. - * @param {MatrixEvent[]} events state events to prepend + * @param events - state events to prepend */ - - setUnknownStateEvents(events) { const unknownStateEvents = events.filter(event => { return !this.events.has(event.getType()) || !this.events.get(event.getType()).has(event.getStateKey()); }); this.setStateEvents(unknownStateEvents); } + /** * Add an array of one or more state MatrixEvents, overwriting any existing - * state with the same {type, stateKey} tuple. Will fire "RoomState.events" + * state with the same `{type, stateKey}` tuple. Will fire "RoomState.events" * for every event added. May fire "RoomState.members" if there are - * m.room.member events. May fire "RoomStateEvent.Marker" if there are - * UNSTABLE_MSC2716_MARKER events. - * @param {MatrixEvent[]} stateEvents a list of state events for this room. - * @param {IMarkerFoundOptions} markerFoundOptions - * @fires module:client~MatrixClient#event:"RoomState.members" - * @fires module:client~MatrixClient#event:"RoomState.newMember" - * @fires module:client~MatrixClient#event:"RoomState.events" - * @fires module:client~MatrixClient#event:"RoomStateEvent.Marker" + * `m.room.member` events. May fire "RoomStateEvent.Marker" if there are + * `UNSTABLE_MSC2716_MARKER` events. + * @param stateEvents - a list of state events for this room. + * + * @remarks + * Fires {@link RoomStateEvent.Members} + * Fires {@link RoomStateEvent.NewMember} + * Fires {@link RoomStateEvent.Events} + * Fires {@link RoomStateEvent.Marker} */ - - setStateEvents(stateEvents, markerFoundOptions) { - this.updateModifiedTime(); // update the core event dict + this.updateModifiedTime(); + // update the core event dict stateEvents.forEach(event => { - if (event.getRoomId() !== this.roomId) { - return; - } - - if (!event.isState()) { - return; - } - + if (event.getRoomId() !== this.roomId || !event.isState()) return; if (_beacon2.M_BEACON_INFO.matches(event.getType())) { this.setBeacon(event); } - const lastStateEvent = this.getStateEventMatching(event); this.setStateEvent(event); - if (event.getType() === _event.EventType.RoomMember) { - this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname); + this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname ?? ""); this.updateThirdPartyTokenCache(event); } - this.emit(RoomStateEvent.Events, event, this, lastStateEvent); }); - this.onBeaconLivenessChange(); // update higher level data structures. This needs to be done AFTER the + this.onBeaconLivenessChange(); + // update higher level data structures. This needs to be done AFTER the // core event dict as these structures may depend on other state events in // the given array (e.g. disambiguating display names in one go to do both // clashing names rather than progressively which only catches 1 of them). - stateEvents.forEach(event => { - if (event.getRoomId() !== this.roomId) { - return; - } - - if (!event.isState()) { - return; - } - + if (event.getRoomId() !== this.roomId || !event.isState()) return; if (event.getType() === _event.EventType.RoomMember) { - const userId = event.getStateKey(); // leave events apparently elide the displayname or avatar_url, + const userId = event.getStateKey(); + + // leave events apparently elide the displayname or avatar_url, // so let's fake one up so that we don't leak user ids // into the timeline - if (event.getContent().membership === "leave" || event.getContent().membership === "ban") { event.getContent().avatar_url = event.getContent().avatar_url || event.getPrevContent().avatar_url; event.getContent().displayname = event.getContent().displayname || event.getPrevContent().displayname; } - const member = this.getOrCreateMember(userId, event); member.setMembershipEvent(event, this); this.updateMember(member); @@ -424,7 +350,6 @@ if (event.getStateKey() !== "") { return; } - const members = Object.values(this.members); members.forEach(member => { // We only propagate `RoomState.members` event if the @@ -432,12 +357,12 @@ // large room suffer from large re-rendering especially when not needed const oldLastModified = member.getLastModifiedTime(); member.setPowerLevelEvent(event); - if (oldLastModified !== member.getLastModifiedTime()) { this.emit(RoomStateEvent.Members, event, this, member); } - }); // assume all our sentinels are now out-of-date + }); + // assume all our sentinels are now out-of-date this.sentinels = {}; } else if (_event.UNSTABLE_MSC2716_MARKER.matches(event.getType())) { this.emit(RoomStateEvent.Marker, event, markerFoundOptions); @@ -445,39 +370,29 @@ }); this.emit(RoomStateEvent.Update, this); } - processBeaconEvents(events, matrixClient) { - if (!events.length || // discard locations if we have no beacons + if (!events.length || + // discard locations if we have no beacons !this.beacons.size) { return; } - const beaconByEventIdDict = [...this.beacons.values()].reduce((dict, beacon) => _objectSpread(_objectSpread({}, dict), {}, { [beacon.beaconInfoId]: beacon }), {}); - const processBeaconRelation = (beaconInfoEventId, event) => { if (!_beacon2.M_BEACON.matches(event.getType())) { return; } - const beacon = beaconByEventIdDict[beaconInfoEventId]; - if (beacon) { beacon.addLocations([event]); } }; - events.forEach(event => { - const relatedToEventId = event.getRelation()?.event_id; // not related to a beacon we know about - // discard - - if (!beaconByEventIdDict[relatedToEventId]) { - return; - } - + const relatedToEventId = event.getRelation()?.event_id; + // not related to a beacon we know about; discard + if (!relatedToEventId || !beaconByEventIdDict[relatedToEventId]) return; matrixClient.decryptEventIfNeeded(event); - if (event.isBeingDecrypted() || event.isDecryptionFailure()) { // add an event listener for once the event is decrypted. event.once(_event2.MatrixEventEvent.Decrypted, async () => { @@ -488,66 +403,56 @@ } }); } + /** * Looks up a member by the given userId, and if it doesn't exist, * create it and emit the `RoomState.newMember` event. * This method makes sure the member is added to the members dictionary * before emitting, as this is done from setStateEvents and setOutOfBandMember. - * @param {string} userId the id of the user to look up - * @param {MatrixEvent} event the membership event for the (new) member. Used to emit. - * @fires module:client~MatrixClient#event:"RoomState.newMember" - * @returns {RoomMember} the member, existing or newly created. + * @param userId - the id of the user to look up + * @param event - the membership event for the (new) member. Used to emit. + * @returns the member, existing or newly created. + * + * @remarks + * Fires {@link RoomStateEvent.NewMember} */ - - getOrCreateMember(userId, event) { let member = this.members[userId]; - if (!member) { - member = new _roomMember.RoomMember(this.roomId, userId); // add member to members before emitting any events, + member = new _roomMember.RoomMember(this.roomId, userId); + // add member to members before emitting any events, // as event handlers often lookup the member - this.members[userId] = member; this.emit(RoomStateEvent.NewMember, event, this, member); } - return member; } - setStateEvent(event) { if (!this.events.has(event.getType())) { this.events.set(event.getType(), new Map()); } - this.events.get(event.getType()).set(event.getStateKey(), event); } + /** * @experimental */ - - setBeacon(event) { const beaconIdentifier = (0, _beacon.getBeaconInfoIdentifier)(event); - if (this.beacons.has(beaconIdentifier)) { const beacon = this.beacons.get(beaconIdentifier); - if (event.isRedacted()) { - if (beacon.beaconInfoId === event.getRedactionEvent()?.['redacts']) { + if (beacon.beaconInfoId === event.getRedactionEvent()?.redacts) { beacon.destroy(); this.beacons.delete(beaconIdentifier); } - return; } - return beacon.update(event); } - if (event.isRedacted()) { return; } - const beacon = new _beacon.Beacon(event); this.reEmitter.reEmit(beacon, [_beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]); this.emit(_beacon.BeaconEvent.New, event, beacon); @@ -555,427 +460,430 @@ beacon.on(_beacon.BeaconEvent.Destroy, this.onBeaconLivenessChange.bind(this)); this.beacons.set(beacon.identifier, beacon); } + /** * @experimental * Check liveness of room beacons * emit RoomStateEvent.BeaconLiveness event */ - - onBeaconLivenessChange() { this._liveBeaconIds = Array.from(this.beacons.values()).filter(beacon => beacon.isLive).map(beacon => beacon.identifier); this.emit(RoomStateEvent.BeaconLiveness, this, this.hasLiveBeacons); } - getStateEventMatching(event) { return this.events.get(event.getType())?.get(event.getStateKey()) ?? null; } - updateMember(member) { // this member may have a power level already, so set it. const pwrLvlEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, ""); - if (pwrLvlEvent) { member.setPowerLevelEvent(pwrLvlEvent); - } // blow away the sentinel which is now outdated - + } + // blow away the sentinel which is now outdated delete this.sentinels[member.userId]; this.members[member.userId] = member; this.joinedMemberCount = null; this.invitedMemberCount = null; } + /** * Get the out-of-band members loading state, whether loading is needed or not. * Note that loading might be in progress and hence isn't needed. - * @return {boolean} whether or not the members of this room need to be loaded + * @returns whether or not the members of this room need to be loaded */ - - needsOutOfBandMembers() { return this.oobMemberFlags.status === OobStatus.NotStarted; } + + /** + * Check if loading of out-of-band-members has completed + * + * @returns true if the full membership list of this room has been loaded. False if it is not started or is in + * progress. + */ + outOfBandMembersReady() { + return this.oobMemberFlags.status === OobStatus.Finished; + } + /** * Mark this room state as waiting for out-of-band members, * ensuring it doesn't ask for them to be requested again * through needsOutOfBandMembers */ - - markOutOfBandMembersStarted() { if (this.oobMemberFlags.status !== OobStatus.NotStarted) { return; } - this.oobMemberFlags.status = OobStatus.InProgress; } + /** * Mark this room state as having failed to fetch out-of-band members */ - - markOutOfBandMembersFailed() { if (this.oobMemberFlags.status !== OobStatus.InProgress) { return; } - this.oobMemberFlags.status = OobStatus.NotStarted; } + /** * Clears the loaded out-of-band members */ - - clearOutOfBandMembers() { let count = 0; Object.keys(this.members).forEach(userId => { const member = this.members[userId]; - if (member.isOutOfBand()) { ++count; delete this.members[userId]; } }); - _logger.logger.log(`LL: RoomState removed ${count} members...`); - this.oobMemberFlags.status = OobStatus.NotStarted; } + /** * Sets the loaded out-of-band members. - * @param {MatrixEvent[]} stateEvents array of membership state events + * @param stateEvents - array of membership state events */ - - setOutOfBandMembers(stateEvents) { _logger.logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`); - if (this.oobMemberFlags.status !== OobStatus.InProgress) { return; } - _logger.logger.log(`LL: RoomState put in finished state ...`); - this.oobMemberFlags.status = OobStatus.Finished; stateEvents.forEach(e => this.setOutOfBandMember(e)); this.emit(RoomStateEvent.Update, this); } + /** * Sets a single out of band member, used by both setOutOfBandMembers and clone - * @param {MatrixEvent} stateEvent membership state event + * @param stateEvent - membership state event */ - - setOutOfBandMember(stateEvent) { if (stateEvent.getType() !== _event.EventType.RoomMember) { return; } - const userId = stateEvent.getStateKey(); - const existingMember = this.getMember(userId); // never replace members received as part of the sync - + const existingMember = this.getMember(userId); + // never replace members received as part of the sync if (existingMember && !existingMember.isOutOfBand()) { return; } - const member = this.getOrCreateMember(userId, stateEvent); - member.setMembershipEvent(stateEvent, this); // needed to know which members need to be stored seperately + member.setMembershipEvent(stateEvent, this); + // needed to know which members need to be stored seperately // as they are not part of the sync accumulator // this is cleared by setMembershipEvent so when it's updated through /sync - member.markOutOfBand(); this.updateDisplayNameCache(member.userId, member.name); this.setStateEvent(stateEvent); this.updateMember(member); this.emit(RoomStateEvent.Members, stateEvent, this, member); } + /** * Set the current typing event for this room. - * @param {MatrixEvent} event The typing event + * @param event - The typing event */ - - setTypingEvent(event) { Object.values(this.members).forEach(function (member) { member.setTypingEvent(event); }); } + /** * Get the m.room.member event which has the given third party invite token. * - * @param {string} token The token - * @return {?MatrixEvent} The m.room.member event or null + * @param token - The token + * @returns The m.room.member event or null */ - - getInviteForThreePidToken(token) { return this.tokenToInvite[token] || null; } + /** * Update the last modified time to the current time. */ - - updateModifiedTime() { this.modified = Date.now(); } + /** * Get the timestamp when this room state was last updated. This timestamp is * updated when this object has received new state events. - * @return {number} The timestamp + * @returns The timestamp */ - - getLastModifiedTime() { return this.modified; } + /** * Get user IDs with the specified or similar display names. - * @param {string} displayName The display name to get user IDs from. - * @return {string[]} An array of user IDs or an empty array. + * @param displayName - The display name to get user IDs from. + * @returns An array of user IDs or an empty array. */ - - getUserIdsWithDisplayName(displayName) { return this.displayNameToUserIds.get(utils.removeHiddenChars(displayName)) ?? []; } + /** * Returns true if userId is in room, event is not redacted and either sender of * mxEvent or has power level sufficient to redact events other than their own. - * @param {MatrixEvent} mxEvent The event to test permission for - * @param {string} userId The user ID of the user to test permission for - * @return {boolean} true if the given used ID can redact given event + * @param mxEvent - The event to test permission for + * @param userId - The user ID of the user to test permission for + * @returns true if the given used ID can redact given event */ - - maySendRedactionForEvent(mxEvent, userId) { const member = this.getMember(userId); - if (!member || member.membership === 'leave') return false; - if (mxEvent.status || mxEvent.isRedacted()) return false; // The user may have been the sender, but they can't redact their own message - // if redactions are blocked. + if (!member || member.membership === "leave") return false; + if (mxEvent.status || mxEvent.isRedacted()) return false; + // The user may have been the sender, but they can't redact their own message + // if redactions are blocked. const canRedact = this.maySendEvent(_event.EventType.RoomRedaction, userId); if (mxEvent.getSender() === userId) return canRedact; - return this.hasSufficientPowerLevelFor('redact', member.powerLevel); + return this.hasSufficientPowerLevelFor("redact", member.powerLevel); } + /** * Returns true if the given power level is sufficient for action - * @param {string} action The type of power level to check - * @param {number} powerLevel The power level of the member - * @return {boolean} true if the given power level is sufficient + * @param action - The type of power level to check + * @param powerLevel - The power level of the member + * @returns true if the given power level is sufficient */ - - hasSufficientPowerLevelFor(action, powerLevel) { const powerLevelsEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, ""); let powerLevels = {}; - if (powerLevelsEvent) { powerLevels = powerLevelsEvent.getContent(); } - let requiredLevel = 50; - if (utils.isNumber(powerLevels[action])) { requiredLevel = powerLevels[action]; } - return powerLevel >= requiredLevel; } + /** * Short-form for maySendEvent('m.room.message', userId) - * @param {string} userId The user ID of the user to test permission for - * @return {boolean} true if the given user ID should be permitted to send + * @param userId - The user ID of the user to test permission for + * @returns true if the given user ID should be permitted to send * message events into the given room. */ - - maySendMessage(userId) { return this.maySendEventOfType(_event.EventType.RoomMessage, userId, false); } + /** * Returns true if the given user ID has permission to send a normal * event of type `eventType` into this room. - * @param {string} eventType The type of event to test - * @param {string} userId The user ID of the user to test permission for - * @return {boolean} true if the given user ID should be permitted to send + * @param eventType - The type of event to test + * @param userId - The user ID of the user to test permission for + * @returns true if the given user ID should be permitted to send * the given type of event into this room, * according to the room's state. */ - - maySendEvent(eventType, userId) { return this.maySendEventOfType(eventType, userId, false); } - /** - * Returns true if the given MatrixClient has permission to send a state - * event of type `stateEventType` into this room. - * @param {string} stateEventType The type of state events to test - * @param {MatrixClient} cli The client to test permission for - * @return {boolean} true if the given client should be permitted to send - * the given type of state event into this room, - * according to the room's state. - */ - + /** + * Returns true if the given MatrixClient has permission to send a state + * event of type `stateEventType` into this room. + * @param stateEventType - The type of state events to test + * @param cli - The client to test permission for + * @returns true if the given client should be permitted to send + * the given type of state event into this room, + * according to the room's state. + */ mayClientSendStateEvent(stateEventType, cli) { - if (cli.isGuest()) { + if (cli.isGuest() || !cli.credentials.userId) { return false; } - return this.maySendStateEvent(stateEventType, cli.credentials.userId); } + /** * Returns true if the given user ID has permission to send a state * event of type `stateEventType` into this room. - * @param {string} stateEventType The type of state events to test - * @param {string} userId The user ID of the user to test permission for - * @return {boolean} true if the given user ID should be permitted to send + * @param stateEventType - The type of state events to test + * @param userId - The user ID of the user to test permission for + * @returns true if the given user ID should be permitted to send * the given type of state event into this room, * according to the room's state. */ - - maySendStateEvent(stateEventType, userId) { return this.maySendEventOfType(stateEventType, userId, true); } + /** * Returns true if the given user ID has permission to send a normal or state * event of type `eventType` into this room. - * @param {string} eventType The type of event to test - * @param {string} userId The user ID of the user to test permission for - * @param {boolean} state If true, tests if the user may send a state + * @param eventType - The type of event to test + * @param userId - The user ID of the user to test permission for + * @param state - If true, tests if the user may send a state event of this type. Otherwise tests whether they may send a regular event. - * @return {boolean} true if the given user ID should be permitted to send + * @returns true if the given user ID should be permitted to send * the given type of event into this room, * according to the room's state. */ - - maySendEventOfType(eventType, userId, state) { - const powerLevelsEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, ''); + const powerLevelsEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, ""); let powerLevels; let eventsLevels = {}; let stateDefault = 0; let eventsDefault = 0; let powerLevel = 0; - if (powerLevelsEvent) { powerLevels = powerLevelsEvent.getContent(); eventsLevels = powerLevels.events || {}; - if (Number.isSafeInteger(powerLevels.state_default)) { stateDefault = powerLevels.state_default; } else { stateDefault = 50; } - const userPowerLevel = powerLevels.users && powerLevels.users[userId]; - if (Number.isSafeInteger(userPowerLevel)) { powerLevel = userPowerLevel; } else if (Number.isSafeInteger(powerLevels.users_default)) { powerLevel = powerLevels.users_default; } - if (Number.isSafeInteger(powerLevels.events_default)) { eventsDefault = powerLevels.events_default; } } - let requiredLevel = state ? stateDefault : eventsDefault; - if (Number.isSafeInteger(eventsLevels[eventType])) { requiredLevel = eventsLevels[eventType]; } - return powerLevel >= requiredLevel; } + /** * Returns true if the given user ID has permission to trigger notification * of type `notifLevelKey` - * @param {string} notifLevelKey The level of notification to test (eg. 'room') - * @param {string} userId The user ID of the user to test permission for - * @return {boolean} true if the given user ID has permission to trigger a + * @param notifLevelKey - The level of notification to test (eg. 'room') + * @param userId - The user ID of the user to test permission for + * @returns true if the given user ID has permission to trigger a * notification of this type. */ - - mayTriggerNotifOfType(notifLevelKey, userId) { const member = this.getMember(userId); - if (!member) { return false; } - - const powerLevelsEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, ''); + const powerLevelsEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, ""); let notifLevel = 50; - if (powerLevelsEvent && powerLevelsEvent.getContent() && powerLevelsEvent.getContent().notifications && utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])) { notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey]; } - return member.powerLevel >= notifLevel; } + /** * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`. - * @returns {string} the join_rule applied to this room + * @returns the join_rule applied to this room */ - - getJoinRule() { const joinRuleEvent = this.getStateEvents(_event.EventType.RoomJoinRules, ""); const joinRuleContent = joinRuleEvent?.getContent() ?? {}; return joinRuleContent["join_rule"] || _partials.JoinRule.Invite; } + /** * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. - * @returns {HistoryVisibility} the history_visibility applied to this room + * @returns the history_visibility applied to this room */ - - getHistoryVisibility() { const historyVisibilityEvent = this.getStateEvents(_event.EventType.RoomHistoryVisibility, ""); const historyVisibilityContent = historyVisibilityEvent?.getContent() ?? {}; return historyVisibilityContent["history_visibility"] || _partials.HistoryVisibility.Shared; } + /** * Returns the guest access based on the m.room.guest_access state event, defaulting to `shared`. - * @returns {GuestAccess} the guest_access applied to this room + * @returns the guest_access applied to this room */ - - getGuestAccess() { const guestAccessEvent = this.getStateEvents(_event.EventType.RoomGuestAccess, ""); const guestAccessContent = guestAccessEvent?.getContent() ?? {}; return guestAccessContent["guest_access"] || _partials.GuestAccess.Forbidden; } + /** + * Find the predecessor room based on this room state. + * + * @param msc3946ProcessDynamicPredecessor - if true, look for an + * m.room.predecessor state event and use it if found (MSC3946). + * @returns null if this room has no predecessor. Otherwise, returns + * the roomId and last eventId of the predecessor room. + * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events + * as well as m.room.create events to find predecessors. + * Note: if an m.predecessor event is used, eventId may be undefined + * since last_known_event_id is optional. + */ + findPredecessor(msc3946ProcessDynamicPredecessor = false) { + // Note: the tests for this function are against Room.findPredecessor, + // which just calls through to here. + + if (msc3946ProcessDynamicPredecessor) { + const predecessorEvent = this.getStateEvents(_event.EventType.RoomPredecessor, ""); + if (predecessorEvent) { + const content = predecessorEvent.getContent(); + const roomId = content.predecessor_room_id; + let eventId = content.last_known_event_id; + if (typeof eventId !== "string") { + eventId = undefined; + } + if (typeof roomId === "string") { + return { + roomId, + eventId + }; + } + } + } + const createEvent = this.getStateEvents(_event.EventType.RoomCreate, ""); + if (createEvent) { + const predecessor = createEvent.getContent()["predecessor"]; + if (predecessor) { + const roomId = predecessor["room_id"]; + if (typeof roomId === "string") { + let eventId = predecessor["event_id"]; + if (typeof eventId !== "string" || eventId === "") { + eventId = undefined; + } + return { + roomId, + eventId + }; + } + } + } + return null; + } updateThirdPartyTokenCache(memberEvent) { if (!memberEvent.getContent().third_party_invite) { return; } - const token = (memberEvent.getContent().third_party_invite.signed || {}).token; - if (!token) { return; } - const threePidInvite = this.getStateEvents(_event.EventType.RoomThirdPartyInvite, token); - if (!threePidInvite) { return; } - this.tokenToInvite[token] = memberEvent; } - updateDisplayNameCache(userId, displayName) { const oldName = this.userIdsToDisplayNames[userId]; delete this.userIdsToDisplayNames[userId]; - if (oldName) { // Remove the old name from the cache. // We clobber the user_id > name lookup but the name -> [user_id] lookup @@ -983,68 +891,20 @@ // the lot. const strippedOldName = utils.removeHiddenChars(oldName); const existingUserIds = this.displayNameToUserIds.get(strippedOldName); - if (existingUserIds) { // remove this user ID from this array const filteredUserIDs = existingUserIds.filter(id => id !== userId); this.displayNameToUserIds.set(strippedOldName, filteredUserIDs); } } - this.userIdsToDisplayNames[userId] = displayName; - const strippedDisplayname = displayName && utils.removeHiddenChars(displayName); // an empty stripped displayname (undefined/'') will be set to MXID in room-member.js - + const strippedDisplayname = displayName && utils.removeHiddenChars(displayName); + // an empty stripped displayname (undefined/'') will be set to MXID in room-member.js if (strippedDisplayname) { const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? []; arr.push(userId); this.displayNameToUserIds.set(strippedDisplayname, arr); } } - } -/** - * Fires whenever the event dictionary in room state is updated. - * @event module:client~MatrixClient#"RoomState.events" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {RoomState} state The room state whose RoomState.events dictionary - * was updated. - * @param {MatrixEvent} prevEvent The event being replaced by the new state, if - * known. Note that this can differ from `getPrevContent()` on the new state event - * as this is the store's view of the last state, not the previous state provided - * by the server. - * @example - * matrixClient.on("RoomState.events", function(event, state, prevEvent){ - * var newStateEvent = event; - * }); - */ - -/** - * Fires whenever a member in the members dictionary is updated in any way. - * @event module:client~MatrixClient#"RoomState.members" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {RoomState} state The room state whose RoomState.members dictionary - * was updated. - * @param {RoomMember} member The room member that was updated. - * @example - * matrixClient.on("RoomState.members", function(event, state, member){ - * var newMembershipState = member.membership; - * }); - */ - -/** - * Fires whenever a member is added to the members dictionary. The RoomMember - * will not be fully populated yet (e.g. no membership state) but will already - * be available in the members dictionary. - * @event module:client~MatrixClient#"RoomState.newMember" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {RoomState} state The room state whose RoomState.members dictionary - * was updated with a new entry. - * @param {RoomMember} member The room member that was added. - * @example - * matrixClient.on("RoomState.newMember", function(event, state, member){ - * // add event listeners on 'member' - * }); - */ - - exports.RoomState = RoomState; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-summary.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-summary.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-summary.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-summary.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,7 +4,6 @@ value: true }); exports.RoomSummary = void 0; - /* Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. @@ -22,27 +21,14 @@ */ /** - * @module models/room-summary - */ - -/** * Construct a new Room Summary. A summary can be used for display on a recent * list, without having to load the entire room list into memory. - * @constructor - * @param {string} roomId Required. The ID of this room. - * @param {Object} info Optional. The summary info. Additional keys are supported. - * @param {string} info.title The title of the room (e.g. m.room.name) - * @param {string} info.desc The description of the room (e.g. - * m.room.topic) - * @param {Number} info.numMembers The number of joined users. - * @param {string[]} info.aliases The list of aliases for this room. - * @param {Number} info.timestamp The timestamp for this room. + * @param roomId - Required. The ID of this room. + * @param info - Optional. The summary info. Additional keys are supported. */ class RoomSummary { constructor(roomId, info) { this.roomId = roomId; } - } - exports.RoomSummary = RoomSummary; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/search-result.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/search-result.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/search-result.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/search-result.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,7 @@ value: true }); exports.SearchResult = void 0; - var _eventContext = require("./event-context"); - /* Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. @@ -23,23 +21,18 @@ limitations under the License. */ -/** - * @module models/search-result - */ class SearchResult { /** * Create a SearchResponse from the response to /search - * @static - * @param {Object} jsonObj - * @param {function} eventMapper - * @return {SearchResult} */ + static fromJson(jsonObj, eventMapper) { const jsonContext = jsonObj.context || {}; let eventsBefore = (jsonContext.events_before || []).map(eventMapper); let eventsAfter = (jsonContext.events_after || []).map(eventMapper); - const context = new _eventContext.EventContext(eventMapper(jsonObj.result)); // Filter out any contextual events which do not correspond to the same timeline (thread or room) + const context = new _eventContext.EventContext(eventMapper(jsonObj.result)); + // Filter out any contextual events which do not correspond to the same timeline (thread or room) const threadRootId = context.ourEvent.threadRootId; eventsBefore = eventsBefore.filter(e => e.threadRootId === threadRootId); eventsAfter = eventsAfter.filter(e => e.threadRootId === threadRootId); @@ -49,22 +42,17 @@ context.setPaginateToken(jsonContext.end, false); return new SearchResult(jsonObj.rank, context); } + /** * Construct a new SearchResult * - * @param {number} rank where this SearchResult ranks in the results - * @param {event-context.EventContext} context the matching event and its + * @param rank - where this SearchResult ranks in the results + * @param context - the matching event and its * context - * - * @constructor */ - - constructor(rank, context) { this.rank = rank; this.context = context; } - } - exports.SearchResult = SearchResult; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/thread.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/thread.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/thread.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/thread.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,173 +3,172 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.ThreadFilterType = exports.ThreadEvent = exports.Thread = exports.THREAD_RELATION_TYPE = exports.FILTER_RELATED_BY_SENDERS = exports.FILTER_RELATED_BY_REL_TYPES = void 0; - -var _matrix = require("../matrix"); - +exports.ThreadFilterType = exports.ThreadEvent = exports.Thread = exports.THREAD_RELATION_TYPE = exports.FeatureSupport = exports.FILTER_RELATED_BY_SENDERS = exports.FILTER_RELATED_BY_REL_TYPES = void 0; +exports.determineFeatureSupport = determineFeatureSupport; +exports.threadFilterTypeToFilter = threadFilterTypeToFilter; +var _client = require("../client"); var _ReEmitter = require("../ReEmitter"); - -var _event = require("./event"); - +var _event = require("../@types/event"); +var _event2 = require("./event"); var _eventTimeline = require("./event-timeline"); - var _eventTimelineSet = require("./event-timeline-set"); - -var _typedEventEmitter = require("./typed-event-emitter"); - +var _room = require("./room"); var _NamespacedValue = require("../NamespacedValue"); - var _logger = require("../logger"); - +var _readReceipt = require("./read-receipt"); +var _read_receipts = require("../@types/read_receipts"); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } let ThreadEvent; exports.ThreadEvent = ThreadEvent; - (function (ThreadEvent) { ThreadEvent["New"] = "Thread.new"; ThreadEvent["Update"] = "Thread.update"; ThreadEvent["NewReply"] = "Thread.newReply"; ThreadEvent["ViewThread"] = "Thread.viewThread"; + ThreadEvent["Delete"] = "Thread.delete"; })(ThreadEvent || (exports.ThreadEvent = ThreadEvent = {})); - -/** - * @experimental - */ -class Thread extends _typedEventEmitter.TypedEventEmitter { +let FeatureSupport; +exports.FeatureSupport = FeatureSupport; +(function (FeatureSupport) { + FeatureSupport[FeatureSupport["None"] = 0] = "None"; + FeatureSupport[FeatureSupport["Experimental"] = 1] = "Experimental"; + FeatureSupport[FeatureSupport["Stable"] = 2] = "Stable"; +})(FeatureSupport || (exports.FeatureSupport = FeatureSupport = {})); +function determineFeatureSupport(stable, unstable) { + if (stable) { + return FeatureSupport.Stable; + } else if (unstable) { + return FeatureSupport.Experimental; + } else { + return FeatureSupport.None; + } +} +class Thread extends _readReceipt.ReadReceipt { /** * A reference to all the events ID at the bottom of the threads */ + + /** + * An array of events to add to the timeline once the thread has been initialised + * with server suppport. + */ + constructor(id, rootEvent, opts) { super(); this.id = id; this.rootEvent = rootEvent; - _defineProperty(this, "timelineSet", void 0); - + _defineProperty(this, "timeline", []); _defineProperty(this, "_currentUserParticipated", false); - _defineProperty(this, "reEmitter", void 0); - _defineProperty(this, "lastEvent", void 0); - _defineProperty(this, "replyCount", 0); - + _defineProperty(this, "lastPendingEvent", void 0); + _defineProperty(this, "pendingReplyCount", 0); _defineProperty(this, "room", void 0); - _defineProperty(this, "client", void 0); - + _defineProperty(this, "pendingEventOrdering", void 0); _defineProperty(this, "initialEventsFetched", !Thread.hasServerSideSupport); - + _defineProperty(this, "replayEvents", []); _defineProperty(this, "onBeforeRedaction", (event, redaction) => { - if (event?.isRelation(THREAD_RELATION_TYPE.name) && this.room.eventShouldLiveIn(event).threadId === this.id && event.getId() !== this.id && // the root event isn't counted in the length so ignore this redaction + if (event?.isRelation(THREAD_RELATION_TYPE.name) && this.room.eventShouldLiveIn(event).threadId === this.id && event.getId() !== this.id && + // the root event isn't counted in the length so ignore this redaction !redaction.status // only respect it when it succeeds ) { this.replyCount--; + this.updatePendingReplyCount(); this.emit(ThreadEvent.Update, this); } }); - - _defineProperty(this, "onRedaction", event => { + _defineProperty(this, "onRedaction", async event => { if (event.threadRootId !== this.id) return; // ignore redactions for other timelines - - const events = [...this.timelineSet.getLiveTimeline().getEvents()].reverse(); - this.lastEvent = events.find(e => !e.isRedacted() && e.isRelation(THREAD_RELATION_TYPE.name)) ?? this.rootEvent; - this.emit(ThreadEvent.Update, this); - }); - - _defineProperty(this, "onEcho", event => { - if (event.threadRootId !== this.id) return; // ignore echoes for other timelines - - if (this.lastEvent === event) return; - if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; // There is a risk that the `localTimestamp` approximation will not be accurate - // when threads are used over federation. That could result in the reply - // count value drifting away from the value returned by the server - - const isThreadReply = event.isRelation(THREAD_RELATION_TYPE.name); - - if (!this.lastEvent || this.lastEvent.isRedacted() || isThreadReply && event.getId() !== this.lastEvent.getId() && event.localTimestamp > this.lastEvent.localTimestamp) { - this.lastEvent = event; - - if (this.lastEvent.getId() !== this.id) { - // This counting only works when server side support is enabled as we started the counting - // from the value returned within the bundled relationship - if (Thread.hasServerSideSupport) { - this.replyCount++; - } - - this.emit(ThreadEvent.NewReply, this, event); + if (this.replyCount <= 0) { + for (const threadEvent of this.timeline) { + this.clearEventMetadata(threadEvent); } + this.lastEvent = this.rootEvent; + this._currentUserParticipated = false; + this.emit(ThreadEvent.Delete, this); + } else { + await this.updateThreadMetadata(); } - - this.emit(ThreadEvent.Update, this); }); - + _defineProperty(this, "onTimelineEvent", (event, room, toStartOfTimeline) => { + // Add a synthesized receipt when paginating forward in the timeline + if (!toStartOfTimeline) { + room.addLocalEchoReceipt(event.getSender(), event, _read_receipts.ReceiptType.Read); + } + this.onEcho(event, toStartOfTimeline ?? false); + }); + _defineProperty(this, "onLocalEcho", event => { + this.onEcho(event, false); + }); + _defineProperty(this, "onEcho", async (event, toStartOfTimeline) => { + if (event.threadRootId !== this.id) return; // ignore echoes for other timelines + if (this.lastEvent === event) return; // ignore duplicate events + await this.updateThreadMetadata(); + if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; // don't send a new reply event for reactions or edits + if (toStartOfTimeline) return; // ignore messages added to the start of the timeline + this.emit(ThreadEvent.NewReply, this, event); + }); if (!opts?.room) { // Logging/debugging for https://github.com/vector-im/element-web/issues/22141 // Hope is that we end up with a more obvious stack trace. throw new Error("element-web#22141: A thread requires a room in order to function"); } - this.room = opts.room; this.client = opts.client; + this.pendingEventOrdering = opts.pendingEventOrdering ?? _client.PendingEventOrdering.Chronological; this.timelineSet = new _eventTimelineSet.EventTimelineSet(this.room, { timelineSupport: true, pendingEvents: true }, this.client, this); this.reEmitter = new _ReEmitter.TypedReEmitter(this); - this.reEmitter.reEmit(this.timelineSet, [_matrix.RoomEvent.Timeline, _matrix.RoomEvent.TimelineReset]); - this.room.on(_matrix.MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.room.on(_matrix.RoomEvent.Redaction, this.onRedaction); - this.room.on(_matrix.RoomEvent.LocalEchoUpdated, this.onEcho); - this.timelineSet.on(_matrix.RoomEvent.Timeline, this.onEcho); - - if (opts.initialEvents) { - this.addEvents(opts.initialEvents, false); - } // even if this thread is thought to be originating from this client, we initialise it as we may be in a - // gappy sync and a thread around this event may already exist. + this.reEmitter.reEmit(this.timelineSet, [_room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset]); + this.room.on(_event2.MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); + this.room.on(_room.RoomEvent.Redaction, this.onRedaction); + this.room.on(_room.RoomEvent.LocalEchoUpdated, this.onLocalEcho); + this.timelineSet.on(_room.RoomEvent.Timeline, this.onTimelineEvent); + this.processReceipts(opts.receipts); - - this.initialiseThread(); - this.rootEvent?.setThread(this); + // even if this thread is thought to be originating from this client, we initialise it as we may be in a + // gappy sync and a thread around this event may already exist. + this.updateThreadMetadata(); + this.setEventMetadata(this.rootEvent); } - async fetchRootEvent() { - this.rootEvent = this.room.findEventById(this.id); // If the rootEvent does not exist in the local stores, then fetch it from the server. - + this.rootEvent = this.room.findEventById(this.id); + // If the rootEvent does not exist in the local stores, then fetch it from the server. try { const eventData = await this.client.fetchRoomEvent(this.roomId, this.id); const mapper = this.client.getEventMapper(); this.rootEvent = mapper(eventData); // will merge with existing event object if such is known } catch (e) { _logger.logger.error("Failed to fetch thread root to construct thread with", e); - } // The root event might be not be visible to the person requesting it. - // If it wasn't fetched successfully the thread will work in "limited" mode and won't - // benefit from all the APIs a homeserver can provide to enhance the thread experience - - - this.rootEvent?.setThread(this); - this.emit(ThreadEvent.Update, this); + } + await this.processEvent(this.rootEvent); } - - static setServerSideSupport(hasServerSideSupport, useStable) { - Thread.hasServerSideSupport = hasServerSideSupport; - - if (!useStable) { + static setServerSideSupport(status) { + Thread.hasServerSideSupport = status; + if (status !== FeatureSupport.Stable) { FILTER_RELATED_BY_SENDERS.setPreferUnstable(true); FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true); THREAD_RELATION_TYPE.setPreferUnstable(true); } } - + static setServerSideListSupport(status) { + Thread.hasServerSideListSupport = status; + } + static setServerSideFwdPaginationSupport(status) { + Thread.hasServerSideFwdPaginationSupport = status; + } get roomState() { return this.room.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); } - addEventToTimeline(event, toStartOfTimeline) { if (!this.findEventById(event.getId())) { this.timelineSet.addEventToTimeline(event, this.liveTimeline, { @@ -177,32 +176,29 @@ fromCache: false, roomState: this.roomState }); + this.timeline = this.events; } } - addEvents(events, toStartOfTimeline) { events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false)); - this.emit(ThreadEvent.Update, this); + this.updateThreadMetadata(); } + /** * Add an event to the thread and updates * the tail/root references if needed * Will fire "Thread.update" - * @param event The event to add - * @param {boolean} toStartOfTimeline whether the event is being added + * @param event - The event to add + * @param toStartOfTimeline - whether the event is being added * to the start (and not the end) of the timeline. - * @param {boolean} emit whether to emit the Update event if the thread was updated or not. + * @param emit - whether to emit the Update event if the thread was updated or not. */ + async addEvent(event, toStartOfTimeline, emit = true) { + this.setEventMetadata(event); + const lastReply = this.lastReply(); + const isNewestReply = !lastReply || event.localTimestamp >= lastReply.localTimestamp; - - addEvent(event, toStartOfTimeline, emit = true) { - event.setThread(this); - - if (!this._currentUserParticipated && event.getSender() === this.client.getUserId()) { - this._currentUserParticipated = true; - } // Add all incoming events to the thread's timeline set when there's no server support - - + // Add all incoming events to the thread's timeline set when there's no server support if (!Thread.hasServerSideSupport) { // all the relevant membership info to hydrate events with a sender // is held in the main room timeline @@ -210,60 +206,183 @@ // timeline set to let it reconcile an event with its relevant RoomMember this.addEventToTimeline(event, toStartOfTimeline); this.client.decryptEventIfNeeded(event, {}); - } else if (!toStartOfTimeline && this.initialEventsFetched && event.localTimestamp > this.lastReply()?.localTimestamp) { - this.fetchEditsWhereNeeded(event); + } else if (!toStartOfTimeline && this.initialEventsFetched && isNewestReply) { this.addEventToTimeline(event, false); - } else if (event.isRelation(_matrix.RelationType.Annotation) || event.isRelation(_matrix.RelationType.Replace)) { + this.fetchEditsWhereNeeded(event); + } else if (event.isRelation(_event.RelationType.Annotation) || event.isRelation(_event.RelationType.Replace)) { + if (!this.initialEventsFetched) { + /** + * A thread can be fully discovered via a single sync response + * And when that's the case we still ask the server to do an initialisation + * as it's the safest to ensure we have everything. + * However when we are in that scenario we might loose annotation or edits + * + * This fix keeps a reference to those events and replay them once the thread + * has been initialised properly. + */ + this.replayEvents?.push(event); + } else { + this.addEventToTimeline(event, toStartOfTimeline); + } // Apply annotations and replace relations to the relations of the timeline only - this.timelineSet.relations.aggregateParentEvent(event); - this.timelineSet.relations.aggregateChildEvent(event, this.timelineSet); + this.timelineSet.relations?.aggregateParentEvent(event); + this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet); return; - } // If no thread support exists we want to count all thread relation - // added as a reply. We can't rely on the bundled relationships count - + } + // If no thread support exists we want to count all thread relation + // added as a reply. We can't rely on the bundled relationships count if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) { this.replyCount++; } - if (emit) { - this.emit(ThreadEvent.Update, this); + this.emit(ThreadEvent.NewReply, this, event); + this.updateThreadMetadata(); + } + } + async processEvent(event) { + if (event) { + this.setEventMetadata(event); + await this.fetchEditsWhereNeeded(event); } + this.timeline = this.events; } + /** + * Processes the receipts that were caught during initial sync + * When clients become aware of a thread, they try to retrieve those read receipts + * and apply them to the current thread + * @param receipts - A collection of the receipts cached from initial sync + */ + processReceipts(receipts = []) { + for (const { + eventId, + receiptType, + userId, + receipt, + synthetic + } of receipts) { + this.addReceiptToStructure(eventId, receiptType, userId, receipt, synthetic); + } + } getRootEventBundledRelationship(rootEvent = this.rootEvent) { return rootEvent?.getServerAggregatedRelation(THREAD_RELATION_TYPE.name); } - - async initialiseThread() { - let bundledRelationship = this.getRootEventBundledRelationship(); - - if (Thread.hasServerSideSupport && !bundledRelationship) { - await this.fetchRootEvent(); - bundledRelationship = this.getRootEventBundledRelationship(); - } - + async processRootEvent() { + const bundledRelationship = this.getRootEventBundledRelationship(); if (Thread.hasServerSideSupport && bundledRelationship) { this.replyCount = bundledRelationship.count; - this._currentUserParticipated = bundledRelationship.current_user_participated; - const event = new _event.MatrixEvent(_objectSpread({ - room_id: this.rootEvent.getRoomId() - }, bundledRelationship.latest_event)); - this.setEventMetadata(event); - event.setThread(this); - this.lastEvent = event; - this.fetchEditsWhereNeeded(event); + this._currentUserParticipated = !!bundledRelationship.current_user_participated; + const mapper = this.client.getEventMapper(); + // re-insert roomId + this.lastEvent = mapper(_objectSpread(_objectSpread({}, bundledRelationship.latest_event), {}, { + room_id: this.roomId + })); + this.updatePendingReplyCount(); + await this.processEvent(this.lastEvent); } + } + updatePendingReplyCount() { + const unfilteredPendingEvents = this.pendingEventOrdering === _client.PendingEventOrdering.Detached ? this.room.getPendingEvents() : this.events; + const pendingEvents = unfilteredPendingEvents.filter(ev => ev.threadRootId === this.id && ev.isRelation(THREAD_RELATION_TYPE.name) && ev.status !== null && ev.getId() !== this.lastEvent?.getId()); + this.lastPendingEvent = pendingEvents.length ? pendingEvents[pendingEvents.length - 1] : undefined; + this.pendingReplyCount = pendingEvents.length; + } + /** + * Reset the live timeline of all timelineSets, and start new ones. + * + *

This is used when /sync returns a 'limited' timeline. 'Limited' means that there's a gap between the messages + * /sync returned, and the last known message in our timeline. In such a case, our live timeline isn't live anymore + * and has to be replaced by a new one. To make sure we can continue paginating our timelines correctly, we have to + * set new pagination tokens on the old and the new timeline. + * + * @param backPaginationToken - token for back-paginating the new timeline + * @param forwardPaginationToken - token for forward-paginating the old live timeline, + * if absent or null, all timelines are reset, removing old ones (including the previous live + * timeline which would otherwise be unable to paginate forwards without this token). + * Removing just the old live timeline whilst preserving previous ones is not supported. + */ + async resetLiveTimeline(backPaginationToken, forwardPaginationToken) { + const oldLive = this.liveTimeline; + this.timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined); + const newLive = this.liveTimeline; + + // FIXME: Remove the following as soon as https://github.com/matrix-org/synapse/issues/14830 is resolved. + // + // The pagination API for thread timelines currently can't handle the type of pagination tokens returned by sync + // + // To make this work anyway, we'll have to transform them into one of the types that the API can handle. + // One option is passing the tokens to /messages, which can handle sync tokens, and returns the right format. + // /messages does not return new tokens on requests with a limit of 0. + // This means our timelines might overlap a slight bit, but that's not an issue, as we deduplicate messages + // anyway. + + let newBackward; + let oldForward; + if (backPaginationToken) { + const res = await this.client.createMessagesRequest(this.roomId, backPaginationToken, 1, _eventTimeline.Direction.Forward); + newBackward = res.end; + } + if (forwardPaginationToken) { + const res = await this.client.createMessagesRequest(this.roomId, forwardPaginationToken, 1, _eventTimeline.Direction.Backward); + oldForward = res.start; + } + // Only replace the token if we don't have paginated away from this position already. This situation doesn't + // occur today, but if the above issue is resolved, we'd have to go down this path. + if (forwardPaginationToken && oldLive.getPaginationToken(_eventTimeline.Direction.Forward) === forwardPaginationToken) { + oldLive.setPaginationToken(oldForward ?? null, _eventTimeline.Direction.Forward); + } + if (backPaginationToken && newLive.getPaginationToken(_eventTimeline.Direction.Backward) === backPaginationToken) { + newLive.setPaginationToken(newBackward ?? null, _eventTimeline.Direction.Backward); + } + } + async updateThreadMetadata() { + this.updatePendingReplyCount(); + if (Thread.hasServerSideSupport) { + // Ensure we show *something* as soon as possible, we'll update it as soon as we get better data, but we + // don't want the thread preview to be empty if we can avoid it + if (!this.initialEventsFetched) { + await this.processRootEvent(); + } + await this.fetchRootEvent(); + } + await this.processRootEvent(); + if (!this.initialEventsFetched) { + this.initialEventsFetched = true; + // fetch initial event to allow proper pagination + try { + // if the thread has regular events, this will just load the last reply. + // if the thread is newly created, this will load the root event. + if (this.replyCount === 0 && this.rootEvent) { + this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null); + this.liveTimeline.setPaginationToken(null, _eventTimeline.Direction.Backward); + } else { + await this.client.paginateEventTimeline(this.liveTimeline, { + backwards: true, + limit: Math.max(1, this.length) + }); + } + for (const event of this.replayEvents) { + this.addEvent(event, false); + } + this.replayEvents = null; + // just to make sure that, if we've created a timeline window for this thread before the thread itself + // existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly. + this.emit(_room.RoomEvent.TimelineReset, this.room, this.timelineSet, true); + } catch (e) { + _logger.logger.error("Failed to load start of newly created thread: ", e); + this.initialEventsFetched = false; + } + } this.emit(ThreadEvent.Update, this); - } // XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084 - + } + // XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084 async fetchEditsWhereNeeded(...events) { return Promise.all(events.filter(e => e.isEncrypted()).map(event => { if (event.isRelation()) return; // skip - relations don't get edits - - return this.client.relations(this.roomId, event.getId(), _matrix.RelationType.Replace, event.getType(), { + return this.client.relations(this.roomId, event.getId(), _event.RelationType.Replace, event.getType(), { limit: 1 }).then(relations => { if (relations.events.length) { @@ -274,123 +393,160 @@ }); })); } - - async fetchInitialEvents() { - if (this.initialEventsFetched) return; - await this.fetchEvents(); - this.initialEventsFetched = true; - } - setEventMetadata(event) { - _eventTimeline.EventTimeline.setEventMetadata(event, this.roomState, false); - - event.setThread(this); + if (event) { + _eventTimeline.EventTimeline.setEventMetadata(event, this.roomState, false); + event.setThread(this); + } } + clearEventMetadata(event) { + if (event) { + event.setThread(undefined); + delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name]; + } + } + /** * Finds an event by ID in the current thread */ - - findEventById(eventId) { - // Check the lastEvent as it may have been created based on a bundled relationship and not in a timeline - if (this.lastEvent?.getId() === eventId) { - return this.lastEvent; - } - return this.timelineSet.findEventById(eventId); } + /** * Return last reply to the thread, if known. */ - - lastReply(matches = () => true) { - for (let i = this.events.length - 1; i >= 0; i--) { - const event = this.events[i]; - + for (let i = this.timeline.length - 1; i >= 0; i--) { + const event = this.timeline[i]; if (matches(event)) { return event; } } - return null; } - get roomId() { return this.room.roomId; } + /** * The number of messages in the thread * Only count rel_type=m.thread as we want to * exclude annotations from that number */ - - get length() { - return this.replyCount; + return this.replyCount + this.pendingReplyCount; } + /** - * A getter for the last event added to the thread, if known. + * A getter for the last event of the thread. + * This might be a synthesized event, if so, it will not emit any events to listeners. */ - - get replyToEvent() { - return this.lastEvent ?? this.lastReply(); + return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply(); } - get events() { return this.liveTimeline.getEvents(); } - has(eventId) { - return this.timelineSet.findEventById(eventId) instanceof _event.MatrixEvent; + return this.timelineSet.findEventById(eventId) instanceof _event2.MatrixEvent; } - get hasCurrentUserParticipated() { return this._currentUserParticipated; } - get liveTimeline() { return this.timelineSet.getLiveTimeline(); } + getUnfilteredTimelineSet() { + return this.timelineSet; + } + addReceipt(event, synthetic) { + throw new Error("Unsupported function on the thread model"); + } - async fetchEvents(opts = { - limit: 20, - direction: _eventTimeline.Direction.Backward - }) { - let { - originalEvent, - events, - prevBatch, - nextBatch - } = await this.client.relations(this.room.roomId, this.id, THREAD_RELATION_TYPE.name, null, opts); // When there's no nextBatch returned with a `from` request we have reached - // the end of the thread, and therefore want to return an empty one - - if (!opts.to && !nextBatch) { - events = [...events, originalEvent]; + /** + * Get the ID of the event that a given user has read up to within this thread, + * or null if we have received no read receipt (at all) from them. + * @param userId - The user ID to get read receipt event ID for + * @param ignoreSynthesized - If true, return only receipts that have been + * sent by the server, not implicit ones generated + * by the JS SDK. + * @returns ID of the latest event that the given user has read, or null. + */ + getEventReadUpTo(userId, ignoreSynthesized) { + const isCurrentUser = userId === this.client.getUserId(); + const lastReply = this.timeline[this.timeline.length - 1]; + if (isCurrentUser && lastReply) { + // If the last activity in a thread is prior to the first threaded read receipt + // sent in the room (suggesting that it was sent before the user started + // using a client that supported threaded read receipts), we want to + // consider this thread as read. + const beforeFirstThreadedReceipt = lastReply.getTs() < this.room.getOldestThreadedReceiptTs(); + const lastReplyId = lastReply.getId(); + // Some unsent events do not have an ID, we do not want to consider them read + if (beforeFirstThreadedReceipt && lastReplyId) { + return lastReplyId; + } } + const readUpToId = super.getEventReadUpTo(userId, ignoreSynthesized); - await this.fetchEditsWhereNeeded(...events); - await Promise.all(events.map(event => { - this.setEventMetadata(event); - return this.client.decryptEventIfNeeded(event); - })); - const prependEvents = (opts.direction ?? _eventTimeline.Direction.Backward) === _eventTimeline.Direction.Backward; - this.timelineSet.addEventsToTimeline(events, prependEvents, this.liveTimeline, prependEvents ? nextBatch : prevBatch); - return { - originalEvent, - events, - prevBatch, - nextBatch - }; + // Check whether the unthreaded read receipt for that user is more recent + // than the read receipt inside that thread. + if (lastReply) { + const unthreadedReceipt = this.room.getLastUnthreadedReceiptFor(userId); + if (!unthreadedReceipt) { + return readUpToId; + } + for (let i = this.timeline?.length - 1; i >= 0; --i) { + const ev = this.timeline[i]; + // If we encounter the `readUpToId` we do not need to look further + // there is no "more recent" unthreaded read receipt + if (ev.getId() === readUpToId) return readUpToId; + + // Inspecting events from most recent to oldest, we're checking + // whether an unthreaded read receipt is more recent that the current event. + // We usually prefer relying on the order of the DAG but in this scenario + // it is not possible and we have to rely on timestamp + if (ev.getTs() < unthreadedReceipt.ts) return ev.getId() ?? readUpToId; + } + } + return readUpToId; } + /** + * Determine if the given user has read a particular event. + * + * It is invalid to call this method with an event that is not part of this thread. + * + * This is not a definitive check as it only checks the events that have been + * loaded client-side at the time of execution. + * @param userId - The user ID to check the read state of. + * @param eventId - The event ID to check if the user read. + * @returns True if the user has read the event, false otherwise. + */ + hasUserReadEvent(userId, eventId) { + if (userId === this.client.getUserId()) { + // Consider an event read if it's part of a thread that is before the + // first threaded receipt sent in that room. It is likely that it is + // part of a thread that was created before MSC3771 was implemented. + // Or before the last unthreaded receipt for the logged in user + const beforeFirstThreadedReceipt = (this.lastReply()?.getTs() ?? 0) < this.room.getOldestThreadedReceiptTs(); + const unthreadedReceiptTs = this.room.getLastUnthreadedReceiptFor(userId)?.ts ?? 0; + const beforeLastUnthreadedReceipt = (this?.lastReply()?.getTs() ?? 0) < unthreadedReceiptTs; + if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) { + return true; + } + } + return super.hasUserReadEvent(userId, eventId); + } + setUnread(type, count) { + return this.room.setThreadUnreadNotificationCount(this.id, type, count); + } } - exports.Thread = Thread; - -_defineProperty(Thread, "hasServerSideSupport", void 0); - +_defineProperty(Thread, "hasServerSideSupport", FeatureSupport.None); +_defineProperty(Thread, "hasServerSideListSupport", FeatureSupport.None); +_defineProperty(Thread, "hasServerSideFwdPaginationSupport", FeatureSupport.None); const FILTER_RELATED_BY_SENDERS = new _NamespacedValue.ServerControlledNamespacedValue("related_by_senders", "io.element.relation_senders"); exports.FILTER_RELATED_BY_SENDERS = FILTER_RELATED_BY_SENDERS; const FILTER_RELATED_BY_REL_TYPES = new _NamespacedValue.ServerControlledNamespacedValue("related_by_rel_types", "io.element.relation_types"); @@ -399,8 +555,15 @@ exports.THREAD_RELATION_TYPE = THREAD_RELATION_TYPE; let ThreadFilterType; exports.ThreadFilterType = ThreadFilterType; - (function (ThreadFilterType) { ThreadFilterType[ThreadFilterType["My"] = 0] = "My"; ThreadFilterType[ThreadFilterType["All"] = 1] = "All"; -})(ThreadFilterType || (exports.ThreadFilterType = ThreadFilterType = {})); \ No newline at end of file +})(ThreadFilterType || (exports.ThreadFilterType = ThreadFilterType = {})); +function threadFilterTypeToFilter(type) { + switch (type) { + case ThreadFilterType.My: + return "participated"; + default: + return "all"; + } +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/typed-event-emitter.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/typed-event-emitter.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/typed-event-emitter.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/typed-event-emitter.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,7 @@ value: true }); exports.TypedEventEmitter = exports.EventEmitterEvents = void 0; - var _events = require("events"); - /* Copyright 2021 The Matrix.org Foundation C.I.C. @@ -25,13 +23,11 @@ // eslint-disable-next-line no-restricted-imports let EventEmitterEvents; exports.EventEmitterEvents = EventEmitterEvents; - (function (EventEmitterEvents) { EventEmitterEvents["NewListener"] = "newListener"; EventEmitterEvents["RemoveListener"] = "removeListener"; EventEmitterEvents["Error"] = "error"; })(EventEmitterEvents || (exports.EventEmitterEvents = EventEmitterEvents = {})); - /** * Typed Event Emitter class which can act as a Base Model for all our model * and communication events. @@ -43,55 +39,41 @@ addListener(event, listener) { return super.addListener(event, listener); } - emit(event, ...args) { return super.emit(event, ...args); } - eventNames() { return super.eventNames(); } - listenerCount(event) { return super.listenerCount(event); } - listeners(event) { return super.listeners(event); } - off(event, listener) { return super.off(event, listener); } - on(event, listener) { return super.on(event, listener); } - once(event, listener) { return super.once(event, listener); } - prependListener(event, listener) { return super.prependListener(event, listener); } - prependOnceListener(event, listener) { return super.prependOnceListener(event, listener); } - removeAllListeners(event) { return super.removeAllListeners(event); } - removeListener(event, listener) { return super.removeListener(event, listener); } - rawListeners(event) { return super.rawListeners(event); } - } - exports.TypedEventEmitter = TypedEventEmitter; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/user.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/user.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/models/user.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/models/user.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,14 +4,12 @@ value: true }); exports.UserEvent = exports.User = void 0; - var _typedEventEmitter = require("./typed-event-emitter"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } let UserEvent; exports.UserEvent = UserEvent; - (function (UserEvent) { UserEvent["DisplayName"] = "User.displayName"; UserEvent["AvatarUrl"] = "User.avatarUrl"; @@ -19,259 +17,189 @@ UserEvent["CurrentlyActive"] = "User.currentlyActive"; UserEvent["LastPresenceTs"] = "User.lastPresenceTs"; })(UserEvent || (exports.UserEvent = UserEvent = {})); - class User extends _typedEventEmitter.TypedEventEmitter { - // XXX these should be read-only + /** + * The 'displayname' of the user if known. + * @privateRemarks + * Should be read-only + */ /** - * Construct a new User. A User must have an ID and can optionally have extra - * information associated with it. - * @constructor - * @param {string} userId Required. The ID of this user. - * @prop {string} userId The ID of the user. - * @prop {Object} info The info object supplied in the constructor. - * @prop {string} displayName The 'displayname' of the user if known. - * @prop {string} avatarUrl The 'avatar_url' of the user if known. - * @prop {string} presence The presence enum if known. - * @prop {string} presenceStatusMsg The presence status message if known. - * @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted - * proactively with the server, or we saw a message from the user - * @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last - * received presence data for this user. We can subtract - * lastActiveAgo from this to approximate an absolute value for - * when a user was last active. - * @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be - * an approximation and that the user should be seen as active 'now' - * @prop {Object} events The events describing this user. - * @prop {MatrixEvent} events.presence The m.presence event for this user. + * The 'avatar_url' of the user if known. + * @privateRemarks + * Should be read-only */ - constructor(userId) { - super(); - this.userId = userId; - _defineProperty(this, "modified", void 0); + /** + * The presence status message if known. + * @privateRemarks + * Should be read-only + */ - _defineProperty(this, "displayName", void 0); + /** + * The presence enum if known. + * @privateRemarks + * Should be read-only + */ - _defineProperty(this, "rawDisplayName", void 0); + /** + * Timestamp (ms since the epoch) for when we last received presence data for this user. + * We can subtract lastActiveAgo from this to approximate an absolute value for when a user was last active. + * @privateRemarks + * Should be read-only + */ - _defineProperty(this, "avatarUrl", void 0); + /** + * The time elapsed in ms since the user interacted proactively with the server, + * or we saw a message from the user + * @privateRemarks + * Should be read-only + */ - _defineProperty(this, "presenceStatusMsg", null); + /** + * Whether we should consider lastActiveAgo to be an approximation + * and that the user should be seen as active 'now' + * @privateRemarks + * Should be read-only + */ - _defineProperty(this, "presence", "offline"); + /** + * The events describing this user. + * @privateRemarks + * Should be read-only + */ + /** + * Construct a new User. A User must have an ID and can optionally have extra information associated with it. + * @param userId - Required. The ID of this user. + */ + constructor(userId) { + super(); + this.userId = userId; + _defineProperty(this, "modified", -1); + _defineProperty(this, "displayName", void 0); + _defineProperty(this, "rawDisplayName", void 0); + _defineProperty(this, "avatarUrl", void 0); + _defineProperty(this, "presenceStatusMsg", void 0); + _defineProperty(this, "presence", "offline"); _defineProperty(this, "lastActiveAgo", 0); - _defineProperty(this, "lastPresenceTs", 0); - _defineProperty(this, "currentlyActive", false); - - _defineProperty(this, "events", { - presence: null, - profile: null - }); - + _defineProperty(this, "events", {}); this.displayName = userId; this.rawDisplayName = userId; - this.avatarUrl = null; this.updateModifiedTime(); } + /** * Update this User with the given presence event. May fire "User.presence", * "User.avatarUrl" and/or "User.displayName" if this event updates this user's * properties. - * @param {MatrixEvent} event The m.presence event. - * @fires module:client~MatrixClient#event:"User.presence" - * @fires module:client~MatrixClient#event:"User.displayName" - * @fires module:client~MatrixClient#event:"User.avatarUrl" + * @param event - The `m.presence` event. + * + * @remarks + * Fires {@link UserEvent.Presence} + * Fires {@link UserEvent.DisplayName} + * Fires {@link UserEvent.AvatarUrl} */ - - setPresenceEvent(event) { if (event.getType() !== "m.presence") { return; } - const firstFire = this.events.presence === null; this.events.presence = event; const eventsToFire = []; - if (event.getContent().presence !== this.presence || firstFire) { eventsToFire.push(UserEvent.Presence); } - if (event.getContent().avatar_url && event.getContent().avatar_url !== this.avatarUrl) { eventsToFire.push(UserEvent.AvatarUrl); } - if (event.getContent().displayname && event.getContent().displayname !== this.displayName) { eventsToFire.push(UserEvent.DisplayName); } - if (event.getContent().currently_active !== undefined && event.getContent().currently_active !== this.currentlyActive) { eventsToFire.push(UserEvent.CurrentlyActive); } - this.presence = event.getContent().presence; eventsToFire.push(UserEvent.LastPresenceTs); - if (event.getContent().status_msg) { this.presenceStatusMsg = event.getContent().status_msg; } - if (event.getContent().displayname) { this.displayName = event.getContent().displayname; } - if (event.getContent().avatar_url) { this.avatarUrl = event.getContent().avatar_url; } - this.lastActiveAgo = event.getContent().last_active_ago; this.lastPresenceTs = Date.now(); this.currentlyActive = event.getContent().currently_active; this.updateModifiedTime(); - - for (let i = 0; i < eventsToFire.length; i++) { - this.emit(eventsToFire[i], event, this); + for (const eventToFire of eventsToFire) { + this.emit(eventToFire, event, this); } } + /** * Manually set this user's display name. No event is emitted in response to this * as there is no underlying MatrixEvent to emit with. - * @param {string} name The new display name. + * @param name - The new display name. */ - - setDisplayName(name) { const oldName = this.displayName; - - if (typeof name === "string") { - this.displayName = name; - } else { - this.displayName = undefined; - } - + this.displayName = name; if (name !== oldName) { this.updateModifiedTime(); } } + /** * Manually set this user's non-disambiguated display name. No event is emitted * in response to this as there is no underlying MatrixEvent to emit with. - * @param {string} name The new display name. + * @param name - The new display name. */ - - setRawDisplayName(name) { - if (typeof name === "string") { - this.rawDisplayName = name; - } else { - this.rawDisplayName = undefined; - } + this.rawDisplayName = name; } + /** * Manually set this user's avatar URL. No event is emitted in response to this * as there is no underlying MatrixEvent to emit with. - * @param {string} url The new avatar URL. + * @param url - The new avatar URL. */ - - setAvatarUrl(url) { const oldUrl = this.avatarUrl; this.avatarUrl = url; - if (url !== oldUrl) { this.updateModifiedTime(); } } + /** * Update the last modified time to the current time. */ - - updateModifiedTime() { this.modified = Date.now(); } + /** * Get the timestamp when this User was last updated. This timestamp is * updated when this User receives a new Presence event which has updated a * property on this object. It is updated before firing events. - * @return {number} The timestamp + * @returns The timestamp */ - - getLastModifiedTime() { return this.modified; } + /** * Get the absolute timestamp when this User was last known active on the server. * It is *NOT* accurate if this.currentlyActive is true. - * @return {number} The timestamp + * @returns The timestamp */ - - getLastActiveTs() { return this.lastPresenceTs - this.lastActiveAgo; } - } -/** - * Fires whenever any user's lastPresenceTs changes, - * ie. whenever any presence event is received for a user. - * @event module:client~MatrixClient#"User.lastPresenceTs" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.lastPresenceTs changed. - * @example - * matrixClient.on("User.lastPresenceTs", function(event, user){ - * var newlastPresenceTs = user.lastPresenceTs; - * }); - */ - -/** - * Fires whenever any user's presence changes. - * @event module:client~MatrixClient#"User.presence" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.presence changed. - * @example - * matrixClient.on("User.presence", function(event, user){ - * var newPresence = user.presence; - * }); - */ - -/** - * Fires whenever any user's currentlyActive changes. - * @event module:client~MatrixClient#"User.currentlyActive" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.currentlyActive changed. - * @example - * matrixClient.on("User.currentlyActive", function(event, user){ - * var newCurrentlyActive = user.currentlyActive; - * }); - */ - -/** - * Fires whenever any user's display name changes. - * @event module:client~MatrixClient#"User.displayName" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.displayName changed. - * @example - * matrixClient.on("User.displayName", function(event, user){ - * var newName = user.displayName; - * }); - */ - -/** - * Fires whenever any user's avatar URL changes. - * @event module:client~MatrixClient#"User.avatarUrl" - * @param {MatrixEvent} event The matrix event which caused this event to fire. - * @param {User} user The user whose User.avatarUrl changed. - * @example - * matrixClient.on("User.avatarUrl", function(event, user){ - * var newUrl = user.avatarUrl; - * }); - */ - - exports.User = User; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/NamespacedValue.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/NamespacedValue.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/NamespacedValue.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/NamespacedValue.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,9 @@ value: true }); exports.UnstableValue = exports.ServerControlledNamespacedValue = exports.NamespacedValue = void 0; - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* Copyright 2021 - 2022 The Matrix.org Foundation C.I.C. @@ -30,122 +30,94 @@ class NamespacedValue { // Stable is optional, but one of the two parameters is required, hence the weird-looking types. // Goal is to to have developers explicitly say there is no stable value (if applicable). + constructor(stable, unstable) { this.stable = stable; this.unstable = unstable; - if (!this.unstable && !this.stable) { throw new Error("One of stable or unstable values must be supplied"); } } - get name() { if (this.stable) { return this.stable; } - return this.unstable; } - get altName() { if (!this.stable) { return null; } - return this.unstable; } - get names() { const names = [this.name]; const altName = this.altName; if (altName) names.push(altName); return names; } - matches(val) { return this.name === val || this.altName === val; - } // this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class - // so we can instantiate `NamespacedValue` as a default type for that namespace. - + } + // this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class + // so we can instantiate `NamespacedValue` as a default type for that namespace. findIn(obj) { - let val; - + let val = undefined; if (this.name) { val = obj?.[this.name]; } - if (!val && this.altName) { val = obj?.[this.altName]; } - return val; } - includedIn(arr) { let included = false; - if (this.name) { included = arr.includes(this.name); } - if (!included && this.altName) { included = arr.includes(this.altName); } - return included; } - } - exports.NamespacedValue = NamespacedValue; - class ServerControlledNamespacedValue extends NamespacedValue { constructor(...args) { super(...args); - _defineProperty(this, "preferUnstable", false); } - setPreferUnstable(preferUnstable) { this.preferUnstable = preferUnstable; } - get name() { if (this.stable && !this.preferUnstable) { return this.stable; } - return this.unstable; } - } + /** * Represents a namespaced value which prioritizes the unstable value over the stable * value. */ - - exports.ServerControlledNamespacedValue = ServerControlledNamespacedValue; - class UnstableValue extends NamespacedValue { // Note: Constructor difference is that `unstable` is *required*. constructor(stable, unstable) { super(stable, unstable); - if (!this.unstable) { throw new Error("Unstable value must be supplied"); } } - get name() { return this.unstable; } - get altName() { return this.stable; } - } - exports.UnstableValue = UnstableValue; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/pushprocessor.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/pushprocessor.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/pushprocessor.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/pushprocessor.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,32 +4,24 @@ value: true }); exports.PushProcessor = void 0; - var _utils = require("./utils"); - var _logger = require("./logger"); - var _PushRules = require("./@types/PushRules"); - var _event = require("./@types/event"); - function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +const RULEKINDS_IN_ORDER = [_PushRules.PushRuleKind.Override, _PushRules.PushRuleKind.ContentSpecific, _PushRules.PushRuleKind.RoomSpecific, _PushRules.PushRuleKind.SenderSpecific, _PushRules.PushRuleKind.Underride]; -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -/** - * @module pushprocessor - */ -const RULEKINDS_IN_ORDER = [_PushRules.PushRuleKind.Override, _PushRules.PushRuleKind.ContentSpecific, _PushRules.PushRuleKind.RoomSpecific, _PushRules.PushRuleKind.SenderSpecific, _PushRules.PushRuleKind.Underride]; // The default override rules to apply to the push rules that arrive from the server. +// The default override rules to apply to the push rules that arrive from the server. // We do this for two reasons: // 1. Synapse is unlikely to send us the push rule in an incremental sync - see // https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for // more details. // 2. We often want to start using push rules ahead of the server supporting them, // and so we can put them here. - const DEFAULT_OVERRIDE_RULES = [{ // For homeservers which don't support MSC2153 yet rule_id: ".m.rule.reaction", @@ -57,71 +49,86 @@ }], actions: [] }]; - +const DEFAULT_UNDERRIDE_RULES = [{ + // For homeservers which don't support MSC3914 yet + rule_id: ".org.matrix.msc3914.rule.room.call", + default: true, + enabled: true, + conditions: [{ + kind: _PushRules.ConditionKind.EventMatch, + key: "type", + pattern: "org.matrix.msc3401.call" + }, { + kind: _PushRules.ConditionKind.CallStarted + }], + actions: [_PushRules.PushRuleActionName.Notify, { + set_tweak: _PushRules.TweakName.Sound, + value: "default" + }] +}]; class PushProcessor { /** * Construct a Push Processor. - * @constructor - * @param {Object} client The Matrix client object to use + * @param client - The Matrix client object to use */ constructor(client) { this.client = client; + _defineProperty(this, "parsedKeys", new Map()); } + + /** + * Maps the original key from the push rules to a list of property names + * after unescaping. + */ + /** * Convert a list of actions into a object with the actions as keys and their values - * eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ] - * becomes { notify: true, tweaks: { sound: 'default' } } - * @param {array} actionList The actions list + * @example + * eg. `[ 'notify', { set_tweak: 'sound', value: 'default' } ]` + * becomes `{ notify: true, tweaks: { sound: 'default' } }` + * @param actionList - The actions list * - * @return {object} A object with key 'notify' (true or false) and an object of actions + * @returns A object with key 'notify' (true or false) and an object of actions */ - - static actionListToActionsObject(actionList) { const actionObj = { notify: false, tweaks: {} }; - - for (let i = 0; i < actionList.length; ++i) { - const action = actionList[i]; - + for (const action of actionList) { if (action === _PushRules.PushRuleActionName.Notify) { actionObj.notify = true; - } else if (typeof action === 'object') { + } else if (typeof action === "object") { if (action.value === undefined) { action.value = true; } - actionObj.tweaks[action.set_tweak] = action.value; } } - return actionObj; } + /** * Rewrites conditions on a client's push rules to match the defaults * where applicable. Useful for upgrading push rules to more strict * conditions when the server is falling behind on defaults. - * @param {object} incomingRules The client's existing push rules - * @returns {object} The rewritten rules + * @param incomingRules - The client's existing push rules + * @returns The rewritten rules */ - - static rewriteDefaultRules(incomingRules) { let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone + // These lines are mostly to make the tests happy. We shouldn't run into these // properties missing in practice. - if (!newRules) newRules = {}; if (!newRules.global) newRules.global = {}; - if (!newRules.global.override) newRules.global.override = []; // Merge the client-level defaults with the ones from the server + if (!newRules.global.override) newRules.global.override = []; + if (!newRules.global.underride) newRules.global.underride = []; + // Merge the client-level defaults with the ones from the server const globalOverrides = newRules.global.override; - for (const override of DEFAULT_OVERRIDE_RULES) { const existingRule = globalOverrides.find(r => r.rule_id === override.rule_id); - if (existingRule) { // Copy over the actions, default, and conditions. Don't touch the user's preference. existingRule.default = override.default; @@ -130,39 +137,84 @@ } else { // Add the rule const ruleId = override.rule_id; - _logger.logger.warn(`Adding default global override for ${ruleId}`); - globalOverrides.push(override); } } - + const globalUnderrides = newRules.global.underride ?? []; + for (const underride of DEFAULT_UNDERRIDE_RULES) { + const existingRule = globalUnderrides.find(r => r.rule_id === underride.rule_id); + if (existingRule) { + // Copy over the actions, default, and conditions. Don't touch the user's preference. + existingRule.default = underride.default; + existingRule.conditions = underride.conditions; + existingRule.actions = underride.actions; + } else { + // Add the rule + const ruleId = underride.rule_id; + _logger.logger.warn(`Adding default global underride for ${ruleId}`); + globalUnderrides.push(underride); + } + } return newRules; } + /** + * Pre-caches the parsed keys for push rules and cleans out any obsolete cache + * entries. Should be called after push rules are updated. + * @param newRules - The new push rules. + */ + updateCachedPushRuleKeys(newRules) { + // These lines are mostly to make the tests happy. We shouldn't run into these + // properties missing in practice. + if (!newRules) newRules = {}; + if (!newRules.global) newRules.global = {}; + if (!newRules.global.override) newRules.global.override = []; + if (!newRules.global.room) newRules.global.room = []; + if (!newRules.global.sender) newRules.global.sender = []; + if (!newRules.global.underride) newRules.global.underride = []; + + // Process the 'key' property on event_match conditions pre-cache the + // values and clean-out any unused values. + const toRemoveKeys = new Set(this.parsedKeys.keys()); + for (const ruleset of [newRules.global.override, newRules.global.room, newRules.global.sender, newRules.global.underride]) { + for (const rule of ruleset) { + if (!rule.conditions) { + continue; + } + for (const condition of rule.conditions) { + if (condition.kind !== _PushRules.ConditionKind.EventMatch) { + continue; + } + + // Ensure we keep this key. + toRemoveKeys.delete(condition.key); + + // Pre-process the key. + this.parsedKeys.set(condition.key, PushProcessor.partsForDottedKey(condition.key)); + } + } + } + // Any keys that were previously cached, but are no longer needed should + // be removed. + toRemoveKeys.forEach(k => this.parsedKeys.delete(k)); + } // $glob: RegExp + matchingRuleFromKindSet(ev, kindset) { - for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) { - const kind = RULEKINDS_IN_ORDER[ruleKindIndex]; + for (const kind of RULEKINDS_IN_ORDER) { const ruleset = kindset[kind]; - if (!ruleset) { continue; } - - for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) { - const rule = ruleset[ruleIndex]; - + for (const rule of ruleset) { if (!rule.enabled) { continue; } - const rawrule = this.templateRuleToRaw(kind, rule); - if (!rawrule) { continue; } - if (this.ruleMatchesEvent(rawrule, ev)) { return _objectSpread(_objectSpread({}, rule), {}, { kind @@ -170,328 +222,368 @@ } } } - return null; } - templateRuleToRaw(kind, tprule) { const rawrule = { - 'rule_id': tprule.rule_id, - 'actions': tprule.actions, - 'conditions': [] + rule_id: tprule.rule_id, + actions: tprule.actions, + conditions: [] }; - switch (kind) { case _PushRules.PushRuleKind.Underride: case _PushRules.PushRuleKind.Override: rawrule.conditions = tprule.conditions; break; - case _PushRules.PushRuleKind.RoomSpecific: if (!tprule.rule_id) { return null; } - rawrule.conditions.push({ - 'kind': _PushRules.ConditionKind.EventMatch, - 'key': 'room_id', - 'value': tprule.rule_id + kind: _PushRules.ConditionKind.EventMatch, + key: "room_id", + value: tprule.rule_id }); break; - case _PushRules.PushRuleKind.SenderSpecific: if (!tprule.rule_id) { return null; } - rawrule.conditions.push({ - 'kind': _PushRules.ConditionKind.EventMatch, - 'key': 'user_id', - 'value': tprule.rule_id + kind: _PushRules.ConditionKind.EventMatch, + key: "user_id", + value: tprule.rule_id }); break; - case _PushRules.PushRuleKind.ContentSpecific: if (!tprule.pattern) { return null; } - rawrule.conditions.push({ - 'kind': _PushRules.ConditionKind.EventMatch, - 'key': 'content.body', - 'pattern': tprule.pattern + kind: _PushRules.ConditionKind.EventMatch, + key: "content.body", + pattern: tprule.pattern }); break; } - return rawrule; } - eventFulfillsCondition(cond, ev) { switch (cond.kind) { case _PushRules.ConditionKind.EventMatch: return this.eventFulfillsEventMatchCondition(cond, ev); - + case _PushRules.ConditionKind.EventPropertyIs: + return this.eventFulfillsEventPropertyIsCondition(cond, ev); case _PushRules.ConditionKind.ContainsDisplayName: return this.eventFulfillsDisplayNameCondition(cond, ev); - case _PushRules.ConditionKind.RoomMemberCount: return this.eventFulfillsRoomMemberCountCondition(cond, ev); - case _PushRules.ConditionKind.SenderNotificationPermission: return this.eventFulfillsSenderNotifPermCondition(cond, ev); - } // unknown conditions: we previously matched all unknown conditions, + case _PushRules.ConditionKind.CallStarted: + case _PushRules.ConditionKind.CallStartedPrefix: + return this.eventFulfillsCallStartedCondition(cond, ev); + } + + // unknown conditions: we previously matched all unknown conditions, // but given that rules can be added to the base rules on a server, // it's probably better to not match unknown conditions. - - return false; } - eventFulfillsSenderNotifPermCondition(cond, ev) { - const notifLevelKey = cond['key']; - + const notifLevelKey = cond["key"]; if (!notifLevelKey) { return false; } - const room = this.client.getRoom(ev.getRoomId()); - if (!room?.currentState) { return false; - } // Note that this should not be the current state of the room but the state at + } + + // Note that this should not be the current state of the room but the state at // the point the event is in the DAG. Unfortunately the js-sdk does not store // this. - - return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender()); } - eventFulfillsRoomMemberCountCondition(cond, ev) { if (!cond.is) { return false; } - const room = this.client.getRoom(ev.getRoomId()); - if (!room || !room.currentState || !room.currentState.members) { return false; } - const memberCount = room.currentState.getJoinedMemberCount(); const m = cond.is.match(/^([=<>]*)(\d*)$/); - if (!m) { return false; } - const ineq = m[1]; const rhs = parseInt(m[2]); - if (isNaN(rhs)) { return false; } - switch (ineq) { - case '': - case '==': + case "": + case "==": return memberCount == rhs; - - case '<': + case "<": return memberCount < rhs; - - case '>': + case ">": return memberCount > rhs; - - case '<=': + case "<=": return memberCount <= rhs; - - case '>=': + case ">=": return memberCount >= rhs; - default: return false; } } - eventFulfillsDisplayNameCondition(cond, ev) { let content = ev.getContent(); - if (ev.isEncrypted() && ev.getClearContent()) { content = ev.getClearContent(); } - - if (!content || !content.body || typeof content.body != 'string') { + if (!content || !content.body || typeof content.body != "string") { return false; } - const room = this.client.getRoom(ev.getRoomId()); - - if (!room || !room.currentState || !room.currentState.members || !room.currentState.getMember(this.client.credentials.userId)) { + const member = room?.currentState?.getMember(this.client.credentials.userId); + if (!member) { return false; } + const displayName = member.name; - const displayName = room.currentState.getMember(this.client.credentials.userId).name; // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay + // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay // as shorthand for [^0-9A-Za-z_]. - - const pat = new RegExp("(^|\\W)" + (0, _utils.escapeRegExp)(displayName) + "(\\W|$)", 'i'); + const pat = new RegExp("(^|\\W)" + (0, _utils.escapeRegExp)(displayName) + "(\\W|$)", "i"); return content.body.search(pat) > -1; } + /** + * Check whether the given event matches the push rule condition by fetching + * the property from the event and comparing against the condition's glob-based + * pattern. + * @param cond - The push rule condition to check for a match. + * @param ev - The event to check for a match. + */ eventFulfillsEventMatchCondition(cond, ev) { if (!cond.key) { return false; } - const val = this.valueForDottedKey(cond.key, ev); - - if (typeof val !== 'string') { + if (typeof val !== "string") { return false; } + // XXX This does not match in a case-insensitive manner. + // + // See https://spec.matrix.org/v1.5/client-server-api/#conditions-1 if (cond.value) { return cond.value === val; } - - if (typeof cond.pattern !== 'string') { + if (typeof cond.pattern !== "string") { return false; } - - const regex = cond.key === 'content.body' ? this.createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)') : this.createCachedRegex('^', cond.pattern, '$'); + const regex = cond.key === "content.body" ? this.createCachedRegex("(^|\\W)", cond.pattern, "(\\W|$)") : this.createCachedRegex("^", cond.pattern, "$"); return !!val.match(regex); } + /** + * Check whether the given event matches the push rule condition by fetching + * the property from the event and comparing exactly against the condition's + * value. + * @param cond - The push rule condition to check for a match. + * @param ev - The event to check for a match. + */ + eventFulfillsEventPropertyIsCondition(cond, ev) { + if (!cond.key || cond.value === undefined) { + return false; + } + return cond.value === this.valueForDottedKey(cond.key, ev); + } + eventFulfillsCallStartedCondition(_cond, ev) { + // Since servers don't support properly sending push notification + // about MSC3401 call events, we do the handling ourselves + return ["m.ring", "m.prompt"].includes(ev.getContent()["m.intent"]) && !("m.terminated" in ev.getContent()) && (ev.getPrevContent()["m.terminated"] !== ev.getContent()["m.terminated"] || (0, _utils.deepCompare)(ev.getPrevContent(), {})); + } createCachedRegex(prefix, glob, suffix) { if (PushProcessor.cachedGlobToRegex[glob]) { return PushProcessor.cachedGlobToRegex[glob]; } + PushProcessor.cachedGlobToRegex[glob] = new RegExp(prefix + (0, _utils.globToRegexp)(glob) + suffix, "i") // Case insensitive + ; - PushProcessor.cachedGlobToRegex[glob] = new RegExp(prefix + (0, _utils.globToRegexp)(glob) + suffix, 'i'); return PushProcessor.cachedGlobToRegex[glob]; } + /** + * Parse the key into the separate fields to search by splitting on + * unescaped ".", and then removing any escape characters. + * + * @param str - The key of the push rule condition: a dotted field. + * @returns The unescaped parts to fetch. + * @internal + */ + static partsForDottedKey(str) { + const result = []; + + // The current field and whether the previous character was the escape + // character (a backslash). + let part = ""; + let escaped = false; + + // Iterate over each character, and decide whether to append to the current + // part (following the escape rules) or to start a new part (based on the + // field separator). + for (const c of str) { + // If the previous character was the escape character (a backslash) + // then decide what to append to the current part. + if (escaped) { + if (c === "\\" || c === ".") { + // An escaped backslash or dot just gets added. + part += c; + } else { + // A character that shouldn't be escaped gets the backslash prepended. + part += "\\" + c; + } + // This always resets being escaped. + escaped = false; + continue; + } + if (c == ".") { + // The field separator creates a new part. + result.push(part); + part = ""; + } else if (c == "\\") { + // A backslash adds no characters, but starts an escape sequence. + escaped = true; + } else { + // Otherwise, just add the current character. + part += c; + } + } + + // Ensure the final part is included. If there's an open escape sequence + // it should be included. + if (escaped) { + part += "\\"; + } + result.push(part); + return result; + } + + /** + * For a dotted field and event, fetch the value at that position, if one + * exists. + * + * @param key - The key of the push rule condition: a dotted field to fetch. + * @param ev - The matrix event to fetch the field from. + * @returns The value at the dotted path given by key. + */ valueForDottedKey(key, ev) { - const parts = key.split('.'); - let val; // special-case the first component to deal with encrypted messages + // The key should already have been parsed via updateCachedPushRuleKeys, + // but if it hasn't (maybe via an old consumer of the SDK which hasn't + // been updated?) then lazily calculate it here. + let parts = this.parsedKeys.get(key); + if (parts === undefined) { + parts = PushProcessor.partsForDottedKey(key); + this.parsedKeys.set(key, parts); + } + let val; + // special-case the first component to deal with encrypted messages const firstPart = parts[0]; - - if (firstPart === 'content') { + let currentIndex = 0; + if (firstPart === "content") { val = ev.getContent(); - parts.shift(); - } else if (firstPart === 'type') { + ++currentIndex; + } else if (firstPart === "type") { val = ev.getType(); - parts.shift(); + ++currentIndex; } else { // use the raw event for any other fields val = ev.event; } - - while (parts.length > 0) { - const thisPart = parts.shift(); - - if ((0, _utils.isNullOrUndefined)(val[thisPart])) { - return null; + for (; currentIndex < parts.length; ++currentIndex) { + // The previous iteration resulted in null or undefined, bail (and + // avoid the type error of attempting to retrieve a property). + if ((0, _utils.isNullOrUndefined)(val)) { + return undefined; } - + const thisPart = parts[currentIndex]; val = val[thisPart]; } - return val; } - matchingRuleForEventWithRulesets(ev, rulesets) { if (!rulesets) { return null; } - if (ev.getSender() === this.client.credentials.userId) { return null; } - return this.matchingRuleFromKindSet(ev, rulesets.global); } - pushActionsForEventAndRulesets(ev, rulesets) { const rule = this.matchingRuleForEventWithRulesets(ev, rulesets); - if (!rule) { return {}; } + const actionObj = PushProcessor.actionListToActionsObject(rule.actions); - const actionObj = PushProcessor.actionListToActionsObject(rule.actions); // Some actions are implicit in some situations: we add those here - + // Some actions are implicit in some situations: we add those here if (actionObj.tweaks.highlight === undefined) { // if it isn't specified, highlight if it's a content // rule but otherwise not actionObj.tweaks.highlight = rule.kind == _PushRules.PushRuleKind.ContentSpecific; } - return actionObj; } - ruleMatchesEvent(rule, ev) { - if (!rule.conditions?.length) return true; - let ret = true; - - for (let i = 0; i < rule.conditions.length; ++i) { - const cond = rule.conditions[i]; // @ts-ignore - - ret &= this.eventFulfillsCondition(cond, ev); - } //console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match")); - - - return ret; + return !rule.conditions?.some(cond => !this.eventFulfillsCondition(cond, ev)); } + /** * Get the user's push actions for the given event - * - * @param {module:models/event.MatrixEvent} ev - * - * @return {PushAction} */ - - actionsForEvent(ev) { return this.pushActionsForEventAndRulesets(ev, this.client.pushRules); } + /** * Get one of the users push rules by its ID * - * @param {string} ruleId The ID of the rule to search for - * @return {object} The push rule, or null if no such rule was found + * @param ruleId - The ID of the rule to search for + * @returns The push rule, or null if no such rule was found */ - - getPushRuleById(ruleId) { - for (const scope of ['global']) { - if (this.client.pushRules[scope] === undefined) continue; + const result = this.getPushRuleAndKindById(ruleId); + return result?.rule ?? null; + } + /** + * Get one of the users push rules by its ID + * + * @param ruleId - The ID of the rule to search for + * @returns rule The push rule, or null if no such rule was found + * @returns kind - The PushRuleKind of the rule to search for + */ + getPushRuleAndKindById(ruleId) { + for (const scope of ["global"]) { + if (this.client.pushRules?.[scope] === undefined) continue; for (const kind of RULEKINDS_IN_ORDER) { if (this.client.pushRules[scope][kind] === undefined) continue; - for (const rule of this.client.pushRules[scope][kind]) { - if (rule.rule_id === ruleId) return rule; + if (rule.rule_id === ruleId) return { + rule, + kind + }; } } } - return null; } - } -/** - * @typedef {Object} PushAction - * @type {Object} - * @property {boolean} notify Whether this event should notify the user or not. - * @property {Object} tweaks How this event should be notified. - * @property {boolean} tweaks.highlight Whether this event should be highlighted - * on the UI. - * @property {boolean} tweaks.sound Whether this notification should produce a - * noise. - */ - - exports.PushProcessor = PushProcessor; - _defineProperty(PushProcessor, "cachedGlobToRegex", {}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/randomstring.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/randomstring.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/randomstring.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/randomstring.js 2023-04-11 06:11:52.000000000 +0000 @@ -6,7 +6,6 @@ exports.randomLowercaseString = randomLowercaseString; exports.randomString = randomString; exports.randomUppercaseString = randomUppercaseString; - /* Copyright 2018 New Vector Ltd Copyright 2019 The Matrix.org Foundation C.I.C. @@ -23,28 +22,23 @@ See the License for the specific language governing permissions and limitations under the License. */ + const LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const DIGITS = "0123456789"; - function randomString(len) { return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS); } - function randomLowercaseString(len) { return randomStringFrom(len, LOWERCASE); } - function randomUppercaseString(len) { return randomStringFrom(len, UPPERCASE); } - function randomStringFrom(len, chars) { let ret = ""; - for (let i = 0; i < len; ++i) { ret += chars.charAt(Math.floor(Math.random() * chars.length)); } - return ret; } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/realtime-callbacks.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/realtime-callbacks.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/realtime-callbacks.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/realtime-callbacks.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,9 +5,7 @@ }); exports.clearTimeout = clearTimeout; exports.setTimeout = setTimeout; - var _logger = require("./logger"); - /* Copyright 2016 OpenMarket Ltd Copyright 2019 The Matrix.org Foundation C.I.C. @@ -33,37 +31,39 @@ * In particular, if a timeout would have fired while the system was suspended, * it will instead fire as soon as possible after resume. */ + // we schedule a callback at least this often, to check if we've missed out on // some wall-clock time due to being suspended. -const TIMER_CHECK_PERIOD_MS = 1000; // counter, for making up ids to return from setTimeout +const TIMER_CHECK_PERIOD_MS = 1000; -let count = 0; // the key for our callback with the real global.setTimeout +// counter, for making up ids to return from setTimeout +let count = 0; -let realCallbackKey; // a sorted list of the callbacks to be run. +// the key for our callback with the real global.setTimeout +let realCallbackKey; +// a sorted list of the callbacks to be run. // each is an object with keys [runAt, func, params, key]. +const callbackList = []; -const callbackList = []; // var debuglog = logger.log.bind(logger); - +// var debuglog = logger.log.bind(logger); +/* istanbul ignore next */ const debuglog = function (...params) {}; + /** * reimplementation of window.setTimeout, which will call the callback if * the wallclock time goes past the deadline. * - * @param {function} func callback to be called after a delay - * @param {Number} delayMs number of milliseconds to delay by + * @param func - callback to be called after a delay + * @param delayMs - number of milliseconds to delay by * - * @return {Number} an identifier for this callback, which may be passed into + * @returns an identifier for this callback, which may be passed into * clearTimeout later. */ - - function setTimeout(func, delayMs, ...params) { delayMs = delayMs || 0; - if (delayMs < 0) { delayMs = 0; } - const runAt = Date.now() + delayMs; const key = count++; debuglog("setTimeout: scheduling cb", key, "at", runAt, "(delay", delayMs, ")"); @@ -72,8 +72,9 @@ func: func, params: params, key: key - }; // figure out where it goes in the list + }; + // figure out where it goes in the list const idx = binarySearch(callbackList, function (el) { return el.runAt - runAt; }); @@ -81,105 +82,90 @@ scheduleRealCallback(); return key; } + /** * reimplementation of window.clearTimeout, which mirrors setTimeout * - * @param {Number} key result from an earlier setTimeout call + * @param key - result from an earlier setTimeout call */ - - function clearTimeout(key) { if (callbackList.length === 0) { return; - } // remove the element from the list - + } + // remove the element from the list let i; - for (i = 0; i < callbackList.length; i++) { const cb = callbackList[i]; - if (cb.key == key) { callbackList.splice(i, 1); break; } - } // iff it was the first one in the list, reschedule our callback. - + } + // iff it was the first one in the list, reschedule our callback. if (i === 0) { scheduleRealCallback(); } -} // use the real global.setTimeout to schedule a callback to runCallbacks. - +} +// use the real global.setTimeout to schedule a callback to runCallbacks. function scheduleRealCallback() { if (realCallbackKey) { global.clearTimeout(realCallbackKey); } - const first = callbackList[0]; - if (!first) { debuglog("scheduleRealCallback: no more callbacks, not rescheduling"); return; } - const timestamp = Date.now(); const delayMs = Math.min(first.runAt - timestamp, TIMER_CHECK_PERIOD_MS); debuglog("scheduleRealCallback: now:", timestamp, "delay:", delayMs); realCallbackKey = global.setTimeout(runCallbacks, delayMs); } - function runCallbacks() { - let cb; const timestamp = Date.now(); - debuglog("runCallbacks: now:", timestamp); // get the list of things to call - - const callbacksToRun = []; // eslint-disable-next-line + debuglog("runCallbacks: now:", timestamp); + // get the list of things to call + const callbacksToRun = []; + // eslint-disable-next-line while (true) { const first = callbackList[0]; - if (!first || first.runAt > timestamp) { break; } - - cb = callbackList.shift(); + const cb = callbackList.shift(); debuglog("runCallbacks: popping", cb.key); callbacksToRun.push(cb); - } // reschedule the real callback before running our functions, to + } + + // reschedule the real callback before running our functions, to // keep the codepaths the same whether or not our functions // register their own setTimeouts. - - scheduleRealCallback(); - - for (let i = 0; i < callbacksToRun.length; i++) { - cb = callbacksToRun[i]; - + for (const cb of callbacksToRun) { try { cb.func.apply(global, cb.params); } catch (e) { - _logger.logger.error("Uncaught exception in callback function", e.stack || e); + _logger.logger.error("Uncaught exception in callback function", e); } } } + /* search in a sorted array. * * returns the index of the last element for which func returns * greater than zero, or array.length if no such element exists. */ - - function binarySearch(array, func) { // min is inclusive, max exclusive. let min = 0; let max = array.length; - while (min < max) { const mid = min + max >> 1; const res = func(array[mid]); - if (res > 0) { // the element at 'mid' is too big; set it as the new max. max = mid; @@ -187,8 +173,7 @@ // the element at 'mid' is too small. 'min' is inclusive, so +1. min = mid + 1; } - } // presumably, min==max now. - - + } + // presumably, min==max now. return min; } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/ReEmitter.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/ReEmitter.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/ReEmitter.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/ReEmitter.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,9 @@ value: true }); exports.TypedReEmitter = exports.ReEmitter = void 0; - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd @@ -24,23 +24,23 @@ See the License for the specific language governing permissions and limitations under the License. */ + // eslint-disable-next-line no-restricted-imports + class ReEmitter { constructor(target) { this.target = target; - _defineProperty(this, "reEmitters", new Map()); - } // Map from emitter to event name to re-emitter + } + // Map from emitter to event name to re-emitter reEmit(source, eventNames) { let reEmittersByEvent = this.reEmitters.get(source); - if (!reEmittersByEvent) { reEmittersByEvent = new Map(); this.reEmitters.set(source, reEmittersByEvent); } - for (const eventName of eventNames) { // We include the source as the last argument for event handlers which may need it, // such as read receipt listeners on the client class which won't have the context @@ -56,15 +56,13 @@ // later by a different part of the code where 'emit' throwing because the app hasn't // added an error handler isn't terribly helpful. (A better fix in retrospect may // have been to just avoid using the event name 'error', but backwards compat...) - if (eventName === 'error' && this.target.listenerCount('error') === 0) return; + if (eventName === "error" && this.target.listenerCount("error") === 0) return; this.target.emit(eventName, ...args, source); }; - source.on(eventName, forSource); reEmittersByEvent.set(eventName, forSource); } } - stopReEmitting(source, eventNames) { const reEmittersByEvent = this.reEmitters.get(source); if (!reEmittersByEvent) return; // We were never re-emitting these events in the first place @@ -73,27 +71,19 @@ source.off(eventName, reEmittersByEvent.get(eventName)); reEmittersByEvent.delete(eventName); } - if (reEmittersByEvent.size === 0) this.reEmitters.delete(source); } - } - exports.ReEmitter = ReEmitter; - class TypedReEmitter extends ReEmitter { constructor(target) { super(target); } - reEmit(source, eventNames) { super.reEmit(source, eventNames); } - stopReEmitting(source, eventNames) { super.stopReEmitting(source, eventNames); } - } - exports.TypedReEmitter = TypedReEmitter; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/index.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,16 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _MSC3903ECDHv2RendezvousChannel = require("./MSC3903ECDHv2RendezvousChannel"); +Object.keys(_MSC3903ECDHv2RendezvousChannel).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _MSC3903ECDHv2RendezvousChannel[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _MSC3903ECDHv2RendezvousChannel[key]; + } + }); +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,180 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MSC3903ECDHv2RendezvousChannel = void 0; +var _ = require(".."); +var _olmlib = require("../../crypto/olmlib"); +var _crypto = require("../../crypto/crypto"); +var _SASDecimal = require("../../crypto/verification/SASDecimal"); +var _NamespacedValue = require("../../NamespacedValue"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +const ECDH_V2 = new _NamespacedValue.UnstableValue("m.rendezvous.v2.curve25519-aes-sha256", "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256"); +async function importKey(key) { + if (!_crypto.subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + const imported = _crypto.subtleCrypto.importKey("raw", key, { + name: "AES-GCM" + }, false, ["encrypt", "decrypt"]); + return imported; +} + +/** + * Implementation of the unstable [MSC3903](https://github.com/matrix-org/matrix-spec-proposals/pull/3903) + * X25519/ECDH key agreement based secure rendezvous channel. + * Note that this is UNSTABLE and may have breaking changes without notice. + */ +class MSC3903ECDHv2RendezvousChannel { + constructor(transport, theirPublicKey, onFailure) { + this.transport = transport; + this.theirPublicKey = theirPublicKey; + this.onFailure = onFailure; + _defineProperty(this, "olmSAS", void 0); + _defineProperty(this, "ourPublicKey", void 0); + _defineProperty(this, "aesKey", void 0); + _defineProperty(this, "connected", false); + this.olmSAS = new global.Olm.SAS(); + this.ourPublicKey = (0, _olmlib.decodeBase64)(this.olmSAS.get_pubkey()); + } + async generateCode(intent) { + if (this.transport.ready) { + throw new Error("Code already generated"); + } + await this.transport.send({ + algorithm: ECDH_V2.name + }); + const rendezvous = { + rendezvous: { + algorithm: ECDH_V2.name, + key: (0, _olmlib.encodeUnpaddedBase64)(this.ourPublicKey), + transport: await this.transport.details() + }, + intent + }; + return rendezvous; + } + async connect() { + if (this.connected) { + throw new Error("Channel already connected"); + } + if (!this.olmSAS) { + throw new Error("Channel closed"); + } + const isInitiator = !this.theirPublicKey; + if (isInitiator) { + // wait for the other side to send us their public key + const rawRes = await this.transport.receive(); + if (!rawRes) { + throw new Error("No response from other device"); + } + const res = rawRes; + const { + key, + algorithm + } = res; + if (!algorithm || !ECDH_V2.matches(algorithm) || !key) { + throw new _.RendezvousError("Unsupported algorithm: " + algorithm, _.RendezvousFailureReason.UnsupportedAlgorithm); + } + this.theirPublicKey = (0, _olmlib.decodeBase64)(key); + } else { + // send our public key unencrypted + await this.transport.send({ + algorithm: ECDH_V2.name, + key: (0, _olmlib.encodeUnpaddedBase64)(this.ourPublicKey) + }); + } + this.connected = true; + this.olmSAS.set_their_key((0, _olmlib.encodeUnpaddedBase64)(this.theirPublicKey)); + const initiatorKey = isInitiator ? this.ourPublicKey : this.theirPublicKey; + const recipientKey = isInitiator ? this.theirPublicKey : this.ourPublicKey; + let aesInfo = ECDH_V2.name; + aesInfo += `|${(0, _olmlib.encodeUnpaddedBase64)(initiatorKey)}`; + aesInfo += `|${(0, _olmlib.encodeUnpaddedBase64)(recipientKey)}`; + const aesKeyBytes = this.olmSAS.generate_bytes(aesInfo, 32); + this.aesKey = await importKey(aesKeyBytes); + + // blank the bytes out to make sure not kept in memory + aesKeyBytes.fill(0); + const rawChecksum = this.olmSAS.generate_bytes(aesInfo, 5); + return (0, _SASDecimal.generateDecimalSas)(Array.from(rawChecksum)).join("-"); + } + async encrypt(data) { + if (!_crypto.subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + const iv = new Uint8Array(32); + _crypto.crypto.getRandomValues(iv); + const encodedData = new _crypto.TextEncoder().encode(JSON.stringify(data)); + const ciphertext = await _crypto.subtleCrypto.encrypt({ + name: "AES-GCM", + iv, + tagLength: 128 + }, this.aesKey, encodedData); + return { + iv: (0, _olmlib.encodeUnpaddedBase64)(iv), + ciphertext: (0, _olmlib.encodeUnpaddedBase64)(ciphertext) + }; + } + async send(payload) { + if (!this.olmSAS) { + throw new Error("Channel closed"); + } + if (!this.aesKey) { + throw new Error("Shared secret not set up"); + } + return this.transport.send(await this.encrypt(payload)); + } + async decrypt({ + iv, + ciphertext + }) { + if (!ciphertext || !iv) { + throw new Error("Missing ciphertext and/or iv"); + } + const ciphertextBytes = (0, _olmlib.decodeBase64)(ciphertext); + if (!_crypto.subtleCrypto) { + throw new Error("Web Crypto is not available"); + } + const plaintext = await _crypto.subtleCrypto.decrypt({ + name: "AES-GCM", + iv: (0, _olmlib.decodeBase64)(iv), + tagLength: 128 + }, this.aesKey, ciphertextBytes); + return JSON.parse(new TextDecoder().decode(new Uint8Array(plaintext))); + } + async receive() { + if (!this.olmSAS) { + throw new Error("Channel closed"); + } + if (!this.aesKey) { + throw new Error("Shared secret not set up"); + } + const rawData = await this.transport.receive(); + if (!rawData) { + return undefined; + } + const data = rawData; + if (data.ciphertext && data.iv) { + return this.decrypt(data); + } + throw new Error("Data received but no ciphertext"); + } + async close() { + if (this.olmSAS) { + this.olmSAS.free(); + this.olmSAS = undefined; + } + } + async cancel(reason) { + try { + await this.transport.cancel(reason); + } finally { + await this.close(); + } + } +} +exports.MSC3903ECDHv2RendezvousChannel = MSC3903ECDHv2RendezvousChannel; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/index.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,82 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _MSC3906Rendezvous = require("./MSC3906Rendezvous"); +Object.keys(_MSC3906Rendezvous).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _MSC3906Rendezvous[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _MSC3906Rendezvous[key]; + } + }); +}); +var _RendezvousChannel = require("./RendezvousChannel"); +Object.keys(_RendezvousChannel).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousChannel[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousChannel[key]; + } + }); +}); +var _RendezvousCode = require("./RendezvousCode"); +Object.keys(_RendezvousCode).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousCode[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousCode[key]; + } + }); +}); +var _RendezvousError = require("./RendezvousError"); +Object.keys(_RendezvousError).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousError[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousError[key]; + } + }); +}); +var _RendezvousFailureReason = require("./RendezvousFailureReason"); +Object.keys(_RendezvousFailureReason).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousFailureReason[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousFailureReason[key]; + } + }); +}); +var _RendezvousIntent = require("./RendezvousIntent"); +Object.keys(_RendezvousIntent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousIntent[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousIntent[key]; + } + }); +}); +var _RendezvousTransport = require("./RendezvousTransport"); +Object.keys(_RendezvousTransport).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _RendezvousTransport[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _RendezvousTransport[key]; + } + }); +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/MSC3906Rendezvous.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/MSC3906Rendezvous.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/MSC3906Rendezvous.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/MSC3906Rendezvous.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,219 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MSC3906Rendezvous = void 0; +var _matrixEventsSdk = require("matrix-events-sdk"); +var _ = require("."); +var _feature = require("../feature"); +var _logger = require("../logger"); +var _utils = require("../utils"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +var PayloadType; +(function (PayloadType) { + PayloadType["Start"] = "m.login.start"; + PayloadType["Finish"] = "m.login.finish"; + PayloadType["Progress"] = "m.login.progress"; +})(PayloadType || (PayloadType = {})); +var Outcome; +(function (Outcome) { + Outcome["Success"] = "success"; + Outcome["Failure"] = "failure"; + Outcome["Verified"] = "verified"; + Outcome["Declined"] = "declined"; + Outcome["Unsupported"] = "unsupported"; +})(Outcome || (Outcome = {})); +const LOGIN_TOKEN_PROTOCOL = new _matrixEventsSdk.UnstableValue("login_token", "org.matrix.msc3906.login_token"); + +/** + * Implements MSC3906 to allow a user to sign in on a new device using QR code. + * This implementation only supports generating a QR code on a device that is already signed in. + * Note that this is UNSTABLE and may have breaking changes without notice. + */ +class MSC3906Rendezvous { + /** + * @param channel - The secure channel used for communication + * @param client - The Matrix client in used on the device already logged in + * @param onFailure - Callback for when the rendezvous fails + */ + constructor(channel, client, onFailure) { + this.channel = channel; + this.client = client; + this.onFailure = onFailure; + _defineProperty(this, "newDeviceId", void 0); + _defineProperty(this, "newDeviceKey", void 0); + _defineProperty(this, "ourIntent", _.RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE); + _defineProperty(this, "_code", void 0); + } + + /** + * Returns the code representing the rendezvous suitable for rendering in a QR code or undefined if not generated yet. + */ + get code() { + return this._code; + } + + /** + * Generate the code including doing partial set up of the channel where required. + */ + async generateCode() { + if (this._code) { + return; + } + this._code = JSON.stringify(await this.channel.generateCode(this.ourIntent)); + } + async startAfterShowingCode() { + const checksum = await this.channel.connect(); + _logger.logger.info(`Connected to secure channel with checksum: ${checksum} our intent is ${this.ourIntent}`); + const features = await (0, _feature.buildFeatureSupportMap)(await this.client.getVersions()); + // determine available protocols + if (features.get(_feature.Feature.LoginTokenRequest) === _feature.ServerSupport.Unsupported) { + _logger.logger.info("Server doesn't support MSC3882"); + await this.send({ + type: PayloadType.Finish, + outcome: Outcome.Unsupported + }); + await this.cancel(_.RendezvousFailureReason.HomeserverLacksSupport); + return undefined; + } + await this.send({ + type: PayloadType.Progress, + protocols: [LOGIN_TOKEN_PROTOCOL.name] + }); + _logger.logger.info("Waiting for other device to chose protocol"); + const { + type, + protocol, + outcome + } = await this.receive(); + if (type === PayloadType.Finish) { + // new device decided not to complete + switch (outcome ?? "") { + case "unsupported": + await this.cancel(_.RendezvousFailureReason.UnsupportedAlgorithm); + break; + default: + await this.cancel(_.RendezvousFailureReason.Unknown); + } + return undefined; + } + if (type !== PayloadType.Progress) { + await this.cancel(_.RendezvousFailureReason.Unknown); + return undefined; + } + if (!protocol || !LOGIN_TOKEN_PROTOCOL.matches(protocol)) { + await this.cancel(_.RendezvousFailureReason.UnsupportedAlgorithm); + return undefined; + } + return checksum; + } + async receive() { + return await this.channel.receive(); + } + async send(payload) { + await this.channel.send(payload); + } + async declineLoginOnExistingDevice() { + _logger.logger.info("User declined sign in"); + await this.send({ + type: PayloadType.Finish, + outcome: Outcome.Declined + }); + } + async approveLoginOnExistingDevice(loginToken) { + // eslint-disable-next-line camelcase + await this.send({ + type: PayloadType.Progress, + login_token: loginToken, + homeserver: this.client.baseUrl + }); + _logger.logger.info("Waiting for outcome"); + const res = await this.receive(); + if (!res) { + return undefined; + } + const { + outcome, + device_id: deviceId, + device_key: deviceKey + } = res; + if (outcome !== "success") { + throw new Error("Linking failed"); + } + this.newDeviceId = deviceId; + this.newDeviceKey = deviceKey; + return deviceId; + } + async verifyAndCrossSignDevice(deviceInfo) { + if (!this.client.crypto) { + throw new Error("Crypto not available on client"); + } + if (!this.newDeviceId) { + throw new Error("No new device ID set"); + } + + // check that keys received from the server for the new device match those received from the device itself + if (deviceInfo.getFingerprint() !== this.newDeviceKey) { + throw new Error(`New device has different keys than expected: ${this.newDeviceKey} vs ${deviceInfo.getFingerprint()}`); + } + const userId = this.client.getUserId(); + if (!userId) { + throw new Error("No user ID set"); + } + // mark the device as verified locally + cross sign + _logger.logger.info(`Marking device ${this.newDeviceId} as verified`); + const info = await this.client.crypto.setDeviceVerification(userId, this.newDeviceId, true, false, true); + const masterPublicKey = this.client.crypto.crossSigningInfo.getId("master"); + await this.send({ + type: PayloadType.Finish, + outcome: Outcome.Verified, + verifying_device_id: this.client.getDeviceId(), + verifying_device_key: this.client.getDeviceEd25519Key(), + master_key: masterPublicKey + }); + return info; + } + + /** + * Verify the device and cross-sign it. + * @param timeout - time in milliseconds to wait for device to come online + * @returns the new device info if the device was verified + */ + async verifyNewDeviceOnExistingDevice(timeout = 10 * 1000) { + if (!this.newDeviceId) { + throw new Error("No new device to sign"); + } + if (!this.newDeviceKey) { + _logger.logger.info("No new device key to sign"); + return undefined; + } + if (!this.client.crypto) { + throw new Error("Crypto not available on client"); + } + const userId = this.client.getUserId(); + if (!userId) { + throw new Error("No user ID set"); + } + let deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); + if (!deviceInfo) { + _logger.logger.info("Going to wait for new device to be online"); + await (0, _utils.sleep)(timeout); + deviceInfo = this.client.crypto.getStoredDevice(userId, this.newDeviceId); + } + if (deviceInfo) { + return await this.verifyAndCrossSignDevice(deviceInfo); + } + throw new Error("Device not online within timeout"); + } + async cancel(reason) { + this.onFailure?.(reason); + await this.channel.cancel(reason); + } + async close() { + await this.channel.close(); + } +} +exports.MSC3906Rendezvous = MSC3906Rendezvous; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousChannel.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousChannel.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousChannel.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousChannel.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousCode.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousCode.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousCode.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousCode.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousError.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousError.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousError.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousError.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,29 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RendezvousError = void 0; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +class RendezvousError extends Error { + constructor(message, code) { + super(message); + this.code = code; + } +} +exports.RendezvousError = RendezvousError; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousFailureReason.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousFailureReason.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousFailureReason.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousFailureReason.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,36 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RendezvousFailureReason = void 0; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +let RendezvousFailureReason; +exports.RendezvousFailureReason = RendezvousFailureReason; +(function (RendezvousFailureReason) { + RendezvousFailureReason["UserDeclined"] = "user_declined"; + RendezvousFailureReason["OtherDeviceNotSignedIn"] = "other_device_not_signed_in"; + RendezvousFailureReason["OtherDeviceAlreadySignedIn"] = "other_device_already_signed_in"; + RendezvousFailureReason["Unknown"] = "unknown"; + RendezvousFailureReason["Expired"] = "expired"; + RendezvousFailureReason["UserCancelled"] = "user_cancelled"; + RendezvousFailureReason["InvalidCode"] = "invalid_code"; + RendezvousFailureReason["UnsupportedAlgorithm"] = "unsupported_algorithm"; + RendezvousFailureReason["DataMismatch"] = "data_mismatch"; + RendezvousFailureReason["UnsupportedTransport"] = "unsupported_transport"; + RendezvousFailureReason["HomeserverLacksSupport"] = "homeserver_lacks_support"; +})(RendezvousFailureReason || (exports.RendezvousFailureReason = RendezvousFailureReason = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousIntent.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousIntent.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousIntent.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousIntent.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,27 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RendezvousIntent = void 0; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +let RendezvousIntent; +exports.RendezvousIntent = RendezvousIntent; +(function (RendezvousIntent) { + RendezvousIntent["LOGIN_ON_NEW_DEVICE"] = "login.start"; + RendezvousIntent["RECIPROCATE_LOGIN_ON_EXISTING_DEVICE"] = "login.reciprocate"; +})(RendezvousIntent || (exports.RendezvousIntent = RendezvousIntent = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousTransport.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousTransport.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousTransport.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/RendezvousTransport.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/index.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,16 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _MSC3886SimpleHttpRendezvousTransport = require("./MSC3886SimpleHttpRendezvousTransport"); +Object.keys(_MSC3886SimpleHttpRendezvousTransport).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _MSC3886SimpleHttpRendezvousTransport[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _MSC3886SimpleHttpRendezvousTransport[key]; + } + }); +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,162 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MSC3886SimpleHttpRendezvousTransport = void 0; +var _matrixEventsSdk = require("matrix-events-sdk"); +var _logger = require("../../logger"); +var _utils = require("../../utils"); +var _ = require(".."); +var _httpApi = require("../../http-api"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +const TYPE = new _matrixEventsSdk.UnstableValue("http.v1", "org.matrix.msc3886.http.v1"); +/** + * Implementation of the unstable [MSC3886](https://github.com/matrix-org/matrix-spec-proposals/pull/3886) + * simple HTTP rendezvous protocol. + * Note that this is UNSTABLE and may have breaking changes without notice. + */ +class MSC3886SimpleHttpRendezvousTransport { + constructor({ + onFailure, + client, + fallbackRzServer, + fetchFn + }) { + _defineProperty(this, "uri", void 0); + _defineProperty(this, "etag", void 0); + _defineProperty(this, "expiresAt", void 0); + _defineProperty(this, "client", void 0); + _defineProperty(this, "fallbackRzServer", void 0); + _defineProperty(this, "fetchFn", void 0); + _defineProperty(this, "cancelled", false); + _defineProperty(this, "_ready", false); + _defineProperty(this, "onFailure", void 0); + this.fetchFn = fetchFn; + this.onFailure = onFailure; + this.client = client; + this.fallbackRzServer = fallbackRzServer; + } + get ready() { + return this._ready; + } + async details() { + if (!this.uri) { + throw new Error("Rendezvous not set up"); + } + return { + type: TYPE.name, + uri: this.uri + }; + } + fetch(resource, options) { + if (this.fetchFn) { + return this.fetchFn(resource, options); + } + return global.fetch(resource, options); + } + async getPostEndpoint() { + try { + if (await this.client.doesServerSupportUnstableFeature("org.matrix.msc3886")) { + return `${this.client.baseUrl}${_httpApi.ClientPrefix.Unstable}/org.matrix.msc3886/rendezvous`; + } + } catch (err) { + _logger.logger.warn("Failed to get unstable features", err); + } + return this.fallbackRzServer; + } + async send(data) { + if (this.cancelled) { + return; + } + const method = this.uri ? "PUT" : "POST"; + const uri = this.uri ?? (await this.getPostEndpoint()); + if (!uri) { + throw new Error("Invalid rendezvous URI"); + } + const headers = { + "content-type": "application/json" + }; + if (this.etag) { + headers["if-match"] = this.etag; + } + const res = await this.fetch(uri, { + method, + headers, + body: JSON.stringify(data) + }); + if (res.status === 404) { + return this.cancel(_.RendezvousFailureReason.Unknown); + } + this.etag = res.headers.get("etag") ?? undefined; + if (method === "POST") { + const location = res.headers.get("location"); + if (!location) { + throw new Error("No rendezvous URI given"); + } + const expires = res.headers.get("expires"); + if (expires) { + this.expiresAt = new Date(expires); + } + // we would usually expect the final `url` to be set by a proper fetch implementation. + // however, if a polyfill based on XHR is used it won't be set, we we use existing URI as fallback + const baseUrl = res.url ?? uri; + // resolve location header which could be relative or absolute + this.uri = new URL(location, `${baseUrl}${baseUrl.endsWith("/") ? "" : "/"}`).href; + this._ready = true; + } + } + async receive() { + if (!this.uri) { + throw new Error("Rendezvous not set up"); + } + // eslint-disable-next-line no-constant-condition + while (true) { + if (this.cancelled) { + return undefined; + } + const headers = {}; + if (this.etag) { + headers["if-none-match"] = this.etag; + } + const poll = await this.fetch(this.uri, { + method: "GET", + headers + }); + if (poll.status === 404) { + this.cancel(_.RendezvousFailureReason.Unknown); + return undefined; + } + + // rely on server expiring the channel rather than checking ourselves + + if (poll.headers.get("content-type") !== "application/json") { + this.etag = poll.headers.get("etag") ?? undefined; + } else if (poll.status === 200) { + this.etag = poll.headers.get("etag") ?? undefined; + return poll.json(); + } + await (0, _utils.sleep)(1000); + } + } + async cancel(reason) { + if (reason === _.RendezvousFailureReason.Unknown && this.expiresAt && this.expiresAt.getTime() < Date.now()) { + reason = _.RendezvousFailureReason.Expired; + } + this.cancelled = true; + this._ready = false; + this.onFailure?.(reason); + if (this.uri && reason === _.RendezvousFailureReason.UserDeclined) { + try { + await this.fetch(this.uri, { + method: "DELETE" + }); + } catch (e) { + _logger.logger.warn(e); + } + } + } +} +exports.MSC3886SimpleHttpRendezvousTransport = MSC3886SimpleHttpRendezvousTransport; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/room-hierarchy.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/room-hierarchy.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/room-hierarchy.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/room-hierarchy.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,14 +4,15 @@ value: true }); exports.RoomHierarchy = void 0; - var _event = require("./@types/event"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } class RoomHierarchy { // Map from room id to list of servers which are listed as a via somewhere in the loaded hierarchy + // Map from room id to list of rooms which claim this room as their child + // Map from room id to object /** @@ -19,54 +20,40 @@ * * A RoomHierarchy instance allows you to easily make use of the /hierarchy API and paginate it. * - * @param {Room} root the root of this hierarchy - * @param {number} pageSize the maximum number of rooms to return per page, can be overridden per load request. - * @param {number} maxDepth the maximum depth to traverse the hierarchy to - * @param {boolean} suggestedOnly whether to only return rooms with suggested=true. - * @constructor + * @param root - the root of this hierarchy + * @param pageSize - the maximum number of rooms to return per page, can be overridden per load request. + * @param maxDepth - the maximum depth to traverse the hierarchy to + * @param suggestedOnly - whether to only return rooms with suggested=true. */ constructor(root, pageSize, maxDepth, suggestedOnly = false) { this.root = root; this.pageSize = pageSize; this.maxDepth = maxDepth; this.suggestedOnly = suggestedOnly; - _defineProperty(this, "viaMap", new Map()); - _defineProperty(this, "backRefs", new Map()); - _defineProperty(this, "roomMap", new Map()); - _defineProperty(this, "loadRequest", void 0); - _defineProperty(this, "nextBatch", void 0); - _defineProperty(this, "_rooms", void 0); - _defineProperty(this, "serverSupportError", void 0); } - get noSupport() { return !!this.serverSupportError; } - get canLoadMore() { return !!this.serverSupportError || !!this.nextBatch || !this._rooms; } - get loading() { return !!this.loadRequest; } - get rooms() { return this._rooms; } - async load(pageSize = this.pageSize) { if (this.loadRequest) return this.loadRequest.then(r => r.rooms); this.loadRequest = this.root.client.getRoomHierarchy(this.root.roomId, pageSize, this.maxDepth, this.suggestedOnly, this.nextBatch); let rooms; - try { ({ rooms, @@ -78,35 +65,32 @@ } else { throw e; } - return []; } finally { - this.loadRequest = null; + this.loadRequest = undefined; } - if (this._rooms) { this._rooms = this._rooms.concat(rooms); } else { this._rooms = rooms; } - rooms.forEach(room => { this.roomMap.set(room.room_id, room); room.children_state.forEach(ev => { if (ev.type !== _event.EventType.SpaceChild) return; - const childRoomId = ev.state_key; // track backrefs for quicker hierarchy navigation + const childRoomId = ev.state_key; + // track backrefs for quicker hierarchy navigation if (!this.backRefs.has(childRoomId)) { this.backRefs.set(childRoomId, []); } + this.backRefs.get(childRoomId).push(room.room_id); - this.backRefs.get(childRoomId).push(room.room_id); // fill viaMap - + // fill viaMap if (Array.isArray(ev.content.via)) { if (!this.viaMap.has(childRoomId)) { this.viaMap.set(childRoomId, new Set()); } - const vias = this.viaMap.get(childRoomId); ev.content.via.forEach(via => vias.add(via)); } @@ -114,32 +98,25 @@ }); return rooms; } - getRelation(parentId, childId) { return this.roomMap.get(parentId)?.children_state.find(e => e.state_key === childId); } - isSuggested(parentId, childId) { return this.getRelation(parentId, childId)?.content.suggested; - } // locally remove a relation as a form of local echo - + } + // locally remove a relation as a form of local echo removeRelation(parentId, childId) { const backRefs = this.backRefs.get(childId); - if (backRefs?.length === 1) { this.backRefs.delete(childId); } else if (backRefs?.length) { this.backRefs.set(childId, backRefs.filter(ref => ref !== parentId)); } - const room = this.roomMap.get(parentId); - if (room) { room.children_state = room.children_state.filter(ev => ev.state_key !== childId); } } - } - exports.RoomHierarchy = RoomHierarchy; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/browserify-index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/browserify-index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/browserify-index.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/browserify-index.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,31 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.initRustCrypto = initRustCrypto; +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* This file replaces rust-crypto/index.ts when the js-sdk is being built for browserify. + * + * It is a stub, so that we do not import the whole of the base64'ed wasm artifact into the browserify bundle. + * It deliberately does nothing except raise an exception. + */ + +async function initRustCrypto(_http, _userId, _deviceId) { + throw new Error("Rust crypto is not supported under browserify."); +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/constants.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/constants.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/constants.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/constants.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,25 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RUST_SDK_STORE_PREFIX = void 0; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** The prefix used on indexeddbs created by rust-crypto */ +const RUST_SDK_STORE_PREFIX = "matrix-js-sdk"; +exports.RUST_SDK_STORE_PREFIX = RUST_SDK_STORE_PREFIX; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/index.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,44 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.initRustCrypto = initRustCrypto; +var RustSdkCryptoJs = _interopRequireWildcard(require("@matrix-org/matrix-sdk-crypto-js")); +var _rustCrypto = require("./rust-crypto"); +var _logger = require("../logger"); +var _constants = require("./constants"); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +async function initRustCrypto(http, userId, deviceId) { + // initialise the rust matrix-sdk-crypto-js, if it hasn't already been done + await RustSdkCryptoJs.initAsync(); + + // enable tracing in the rust-sdk + new RustSdkCryptoJs.Tracing(RustSdkCryptoJs.LoggerLevel.Debug).turnOn(); + const u = new RustSdkCryptoJs.UserId(userId); + const d = new RustSdkCryptoJs.DeviceId(deviceId); + _logger.logger.info("Init OlmMachine"); + + // TODO: use the pickle key for the passphrase + const olmMachine = await RustSdkCryptoJs.OlmMachine.initialize(u, d, _constants.RUST_SDK_STORE_PREFIX, "test pass"); + const rustCrypto = new _rustCrypto.RustCrypto(olmMachine, http, userId, deviceId); + _logger.logger.info("Completed rust crypto-sdk setup"); + return rustCrypto; +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/KeyClaimManager.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/KeyClaimManager.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/KeyClaimManager.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/KeyClaimManager.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,75 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.KeyClaimManager = void 0; +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * KeyClaimManager: linearises calls to OlmMachine.getMissingSessions to avoid races + * + * We have one of these per `RustCrypto` (and hence per `MatrixClient`). + */ +class KeyClaimManager { + constructor(olmMachine, outgoingRequestProcessor) { + this.olmMachine = olmMachine; + this.outgoingRequestProcessor = outgoingRequestProcessor; + _defineProperty(this, "currentClaimPromise", void 0); + _defineProperty(this, "stopped", false); + this.currentClaimPromise = Promise.resolve(); + } + + /** + * Tell the KeyClaimManager to immediately stop processing requests. + * + * Any further calls, and any still in the queue, will fail with an error. + */ + stop() { + this.stopped = true; + } + + /** + * Given a list of users, attempt to ensure that we have Olm Sessions active with each of their devices + * + * If we don't have an active olm session, we will claim a one-time key and start one. + * + * @param userList - list of userIDs to claim + */ + ensureSessionsForUsers(userList) { + // The Rust-SDK requires that we only have one getMissingSessions process in flight at once. This little dance + // ensures that, by only having one call to ensureSessionsForUsersInner active at once (and making them + // queue up in order). + const prom = this.currentClaimPromise.finally(() => this.ensureSessionsForUsersInner(userList)); + this.currentClaimPromise = prom; + return prom; + } + async ensureSessionsForUsersInner(userList) { + // bail out quickly if we've been stopped. + if (this.stopped) { + throw new Error(`Cannot ensure Olm sessions: shutting down`); + } + const claimRequest = await this.olmMachine.getMissingSessions(userList); + if (claimRequest) { + await this.outgoingRequestProcessor.makeOutgoingRequest(claimRequest); + } + } +} +exports.KeyClaimManager = KeyClaimManager; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/OutgoingRequestProcessor.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/OutgoingRequestProcessor.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/OutgoingRequestProcessor.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/OutgoingRequestProcessor.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,93 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.OutgoingRequestProcessor = void 0; +var _matrixSdkCryptoJs = require("@matrix-org/matrix-sdk-crypto-js"); +var _logger = require("../logger"); +var _httpApi = require("../http-api"); +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * OutgoingRequestManager: turns `OutgoingRequest`s from the rust sdk into HTTP requests + * + * We have one of these per `RustCrypto` (and hence per `MatrixClient`), not that it does anything terribly complicated. + * It's responsible for: + * + * * holding the reference to the `MatrixHttpApi` + * * turning `OutgoingRequest`s from the rust backend into HTTP requests, and sending them + * * sending the results of such requests back to the rust backend. + */ +class OutgoingRequestProcessor { + constructor(olmMachine, http) { + this.olmMachine = olmMachine; + this.http = http; + } + async makeOutgoingRequest(msg) { + let resp; + + /* refer https://docs.rs/matrix-sdk-crypto/0.6.0/matrix_sdk_crypto/requests/enum.OutgoingRequests.html + * for the complete list of request types + */ + if (msg instanceof _matrixSdkCryptoJs.KeysUploadRequest) { + resp = await this.rawJsonRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/upload", {}, msg.body); + } else if (msg instanceof _matrixSdkCryptoJs.KeysQueryRequest) { + resp = await this.rawJsonRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/query", {}, msg.body); + } else if (msg instanceof _matrixSdkCryptoJs.KeysClaimRequest) { + resp = await this.rawJsonRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/claim", {}, msg.body); + } else if (msg instanceof _matrixSdkCryptoJs.SignatureUploadRequest) { + resp = await this.rawJsonRequest(_httpApi.Method.Post, "/_matrix/client/v3/keys/signatures/upload", {}, msg.body); + } else if (msg instanceof _matrixSdkCryptoJs.KeysBackupRequest) { + resp = await this.rawJsonRequest(_httpApi.Method.Put, "/_matrix/client/v3/room_keys/keys", {}, msg.body); + } else if (msg instanceof _matrixSdkCryptoJs.ToDeviceRequest) { + const path = `/_matrix/client/v3/sendToDevice/${encodeURIComponent(msg.event_type)}/` + encodeURIComponent(msg.txn_id); + resp = await this.rawJsonRequest(_httpApi.Method.Put, path, {}, msg.body); + } else if (msg instanceof _matrixSdkCryptoJs.RoomMessageRequest) { + const path = `/_matrix/client/v3/room/${encodeURIComponent(msg.room_id)}/send/` + `${encodeURIComponent(msg.event_type)}/${encodeURIComponent(msg.txn_id)}`; + resp = await this.rawJsonRequest(_httpApi.Method.Put, path, {}, msg.body); + } else { + _logger.logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg)); + resp = ""; + } + if (msg.id) { + await this.olmMachine.markRequestAsSent(msg.id, msg.type, resp); + } + } + async rawJsonRequest(method, path, queryParams, body) { + const opts = { + // inhibit the JSON stringification and parsing within HttpApi. + json: false, + // nevertheless, we are sending, and accept, JSON. + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + }, + // we use the full prefix + prefix: "" + }; + try { + const response = await this.http.authedRequest(method, path, queryParams, body, opts); + _logger.logger.info(`rust-crypto: successfully made HTTP request: ${method} ${path}`); + return response; + } catch (e) { + _logger.logger.warn(`rust-crypto: error making HTTP request: ${method} ${path}: ${e}`); + throw e; + } + } +} +exports.OutgoingRequestProcessor = OutgoingRequestProcessor; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/RoomEncryptor.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/RoomEncryptor.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/RoomEncryptor.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/RoomEncryptor.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,93 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RoomEncryptor = void 0; +var _matrixSdkCryptoJs = require("@matrix-org/matrix-sdk-crypto-js"); +var _event = require("../@types/event"); +var _logger = require("../logger"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/** + * RoomEncryptor: responsible for encrypting messages to a given room + */ +class RoomEncryptor { + /** + * @param olmMachine - The rust-sdk's OlmMachine + * @param keyClaimManager - Our KeyClaimManager, which manages the queue of one-time-key claim requests + * @param room - The room we want to encrypt for + * @param encryptionSettings - body of the m.room.encryption event currently in force in this room + */ + constructor(olmMachine, keyClaimManager, room, encryptionSettings) { + this.olmMachine = olmMachine; + this.keyClaimManager = keyClaimManager; + this.room = room; + this.encryptionSettings = encryptionSettings; + _defineProperty(this, "prefixedLogger", void 0); + this.prefixedLogger = _logger.logger.withPrefix(`[${room.roomId} encryption]`); + } + + /** + * Handle a new `m.room.encryption` event in this room + * + * @param config - The content of the encryption event + */ + onCryptoEvent(config) { + if (JSON.stringify(this.encryptionSettings) != JSON.stringify(config)) { + this.prefixedLogger.error(`Ignoring m.room.encryption event which requests a change of config`); + } + } + + /** + * Handle a new `m.room.member` event in this room + * + * @param member - new membership state + */ + onRoomMembership(member) { + this.prefixedLogger.debug(`${member.membership} event for ${member.userId}`); + if (member.membership == "join" || member.membership == "invite" && this.room.shouldEncryptForInvitedMembers()) { + // make sure we are tracking the deviceList for this user + this.prefixedLogger.debug(`starting to track devices for: ${member.userId}`); + this.olmMachine.updateTrackedUsers([new _matrixSdkCryptoJs.UserId(member.userId)]); + } + + // TODO: handle leaves (including our own) + } + + /** + * Prepare to encrypt events in this room. + * + * This ensures that we have a megolm session ready to use and that we have shared its key with all the devices + * in the room. + */ + async ensureEncryptionSession() { + if (this.encryptionSettings.algorithm !== "m.megolm.v1.aes-sha2") { + throw new Error(`Cannot encrypt in ${this.room.roomId} for unsupported algorithm '${this.encryptionSettings.algorithm}'`); + } + const members = await this.room.getEncryptionTargetMembers(); + this.prefixedLogger.debug(`Encrypting for users (shouldEncryptForInvitedMembers: ${this.room.shouldEncryptForInvitedMembers()}):`, members.map(u => `${u.userId} (${u.membership})`)); + const userList = members.map(u => new _matrixSdkCryptoJs.UserId(u.userId)); + await this.keyClaimManager.ensureSessionsForUsers(userList); + const rustEncryptionSettings = new _matrixSdkCryptoJs.EncryptionSettings(); + /* FIXME historyVisibility, rotation, etc */ + + await this.olmMachine.shareRoomKey(new _matrixSdkCryptoJs.RoomId(this.room.roomId), userList, rustEncryptionSettings); + } + + /** + * Encrypt an event for this room + * + * This will ensure that we have a megolm session for this room, share it with the devices in the room, and + * then encrypt the event using the session. + * + * @param event - Event to be encrypted. + */ + async encryptEvent(event) { + await this.ensureEncryptionSession(); + const encryptedContent = await this.olmMachine.encryptRoomEvent(new _matrixSdkCryptoJs.RoomId(this.room.roomId), event.getType(), JSON.stringify(event.getContent())); + event.makeEncrypted(_event.EventType.RoomMessageEncrypted, JSON.parse(encryptedContent), this.olmMachine.identityKeys.curve25519.toBase64(), this.olmMachine.identityKeys.ed25519.toBase64()); + } +} +exports.RoomEncryptor = RoomEncryptor; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/rust-crypto.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/rust-crypto.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/rust-crypto.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/rust-crypto/rust-crypto.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,234 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RustCrypto = void 0; +var RustSdkCryptoJs = _interopRequireWildcard(require("@matrix-org/matrix-sdk-crypto-js")); +var _logger = require("../logger"); +var _CrossSigning = require("../crypto/CrossSigning"); +var _RoomEncryptor = require("./RoomEncryptor"); +var _OutgoingRequestProcessor = require("./OutgoingRequestProcessor"); +var _KeyClaimManager = require("./KeyClaimManager"); +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +/** + * An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto. + */ +class RustCrypto { + /** whether {@link stop} has been called */ + + /** whether {@link outgoingRequestLoop} is currently running */ + + /** mapping of roomId → encryptor class */ + + constructor(olmMachine, http, _userId, _deviceId) { + this.olmMachine = olmMachine; + _defineProperty(this, "globalBlacklistUnverifiedDevices", false); + _defineProperty(this, "globalErrorOnUnknownDevices", false); + _defineProperty(this, "stopped", false); + _defineProperty(this, "outgoingRequestLoopRunning", false); + _defineProperty(this, "roomEncryptors", {}); + _defineProperty(this, "keyClaimManager", void 0); + _defineProperty(this, "outgoingRequestProcessor", void 0); + this.outgoingRequestProcessor = new _OutgoingRequestProcessor.OutgoingRequestProcessor(olmMachine, http); + this.keyClaimManager = new _KeyClaimManager.KeyClaimManager(olmMachine, this.outgoingRequestProcessor); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // CryptoBackend implementation + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + stop() { + // stop() may be called multiple times, but attempting to close() the OlmMachine twice + // will cause an error. + if (this.stopped) { + return; + } + this.stopped = true; + this.keyClaimManager.stop(); + + // make sure we close() the OlmMachine; doing so means that all the Rust objects will be + // cleaned up; in particular, the indexeddb connections will be closed, which means they + // can then be deleted. + this.olmMachine.close(); + } + prepareToEncrypt(room) { + const encryptor = this.roomEncryptors[room.roomId]; + if (encryptor) { + encryptor.ensureEncryptionSession(); + } + } + async encryptEvent(event, _room) { + const roomId = event.getRoomId(); + const encryptor = this.roomEncryptors[roomId]; + if (!encryptor) { + throw new Error(`Cannot encrypt event in unconfigured room ${roomId}`); + } + await encryptor.encryptEvent(event); + } + async decryptEvent(event) { + const roomId = event.getRoomId(); + if (!roomId) { + // presumably, a to-device message. These are normally decrypted in preprocessToDeviceMessages + // so the fact it has come back here suggests that decryption failed. + // + // once we drop support for the libolm crypto implementation, we can stop passing to-device messages + // through decryptEvent and hence get rid of this case. + throw new Error("to-device event was not decrypted in preprocessToDeviceMessages"); + } + const res = await this.olmMachine.decryptRoomEvent(JSON.stringify({ + event_id: event.getId(), + type: event.getWireType(), + sender: event.getSender(), + state_key: event.getStateKey(), + content: event.getWireContent(), + origin_server_ts: event.getTs() + }), new RustSdkCryptoJs.RoomId(event.getRoomId())); + return { + clearEvent: JSON.parse(res.event), + claimedEd25519Key: res.senderClaimedEd25519Key, + senderCurve25519Key: res.senderCurve25519Key, + forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain + }; + } + getEventEncryptionInfo(event) { + // TODO: make this work properly. Or better, replace it. + + const ret = {}; + ret.senderKey = event.getSenderKey() ?? undefined; + ret.algorithm = event.getWireContent().algorithm; + if (!ret.senderKey || !ret.algorithm) { + ret.encrypted = false; + return ret; + } + ret.encrypted = true; + ret.authenticated = true; + ret.mismatchedSender = true; + return ret; + } + async userHasCrossSigningKeys() { + // TODO + return false; + } + async exportRoomKeys() { + // TODO + return []; + } + checkUserTrust(userId) { + // TODO + return new _CrossSigning.UserTrustLevel(false, false, false); + } + checkDeviceTrust(userId, deviceId) { + // TODO + return new _CrossSigning.DeviceTrustLevel(false, false, false, false); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // SyncCryptoCallbacks implementation + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** called by the sync loop to preprocess incoming to-device messages + * + * @param events - the received to-device messages + * @returns A list of preprocessed to-device messages. + */ + async preprocessToDeviceMessages(events) { + // send the received to-device messages into receiveSyncChanges. We have no info on device-list changes, + // one-time-keys, or fallback keys, so just pass empty data. + const result = await this.olmMachine.receiveSyncChanges(JSON.stringify(events), new RustSdkCryptoJs.DeviceLists(), new Map(), new Set()); + + // receiveSyncChanges returns a JSON-encoded list of decrypted to-device messages. + return JSON.parse(result); + } + + /** called by the sync loop on m.room.encrypted events + * + * @param room - in which the event was received + * @param event - encryption event to be processed + */ + async onCryptoEvent(room, event) { + const config = event.getContent(); + const existingEncryptor = this.roomEncryptors[room.roomId]; + if (existingEncryptor) { + existingEncryptor.onCryptoEvent(config); + } else { + this.roomEncryptors[room.roomId] = new _RoomEncryptor.RoomEncryptor(this.olmMachine, this.keyClaimManager, room, config); + } + + // start tracking devices for any users already known to be in this room. + const members = await room.getEncryptionTargetMembers(); + _logger.logger.debug(`[${room.roomId} encryption] starting to track devices for: `, members.map(u => `${u.userId} (${u.membership})`)); + await this.olmMachine.updateTrackedUsers(members.map(u => new RustSdkCryptoJs.UserId(u.userId))); + } + + /** called by the sync loop after processing each sync. + * + * TODO: figure out something equivalent for sliding sync. + * + * @param syncState - information on the completed sync. + */ + onSyncCompleted(syncState) { + // Processing the /sync may have produced new outgoing requests which need sending, so kick off the outgoing + // request loop, if it's not already running. + this.outgoingRequestLoop(); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Other public functions + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + /** called by the MatrixClient on a room membership event + * + * @param event - The matrix event which caused this event to fire. + * @param member - The member whose RoomMember.membership changed. + * @param oldMembership - The previous membership state. Null if it's a new member. + */ + onRoomMembership(event, member, oldMembership) { + const enc = this.roomEncryptors[event.getRoomId()]; + if (!enc) { + // not encrypting in this room + return; + } + enc.onRoomMembership(member); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Outgoing requests + // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + async outgoingRequestLoop() { + if (this.outgoingRequestLoopRunning) { + return; + } + this.outgoingRequestLoopRunning = true; + try { + while (!this.stopped) { + const outgoingRequests = await this.olmMachine.outgoingRequests(); + if (outgoingRequests.length == 0 || this.stopped) { + // no more messages to send (or we have been told to stop): exit the loop + return; + } + for (const msg of outgoingRequests) { + await this.outgoingRequestProcessor.makeOutgoingRequest(msg); + } + } + } catch (e) { + _logger.logger.error("Error processing outgoing-message requests from rust crypto-sdk", e); + } finally { + this.outgoingRequestLoopRunning = false; + } + } +} +exports.RustCrypto = RustCrypto; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/scheduler.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/scheduler.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/scheduler.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/scheduler.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,33 +4,17 @@ value: true }); exports.MatrixScheduler = void 0; - var utils = _interopRequireWildcard(require("./utils")); - var _logger = require("./logger"); - var _event = require("./@types/event"); - +var _httpApi = require("./http-api"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const DEBUG = false; // set true to enable console logging. -/** - * Construct a scheduler for Matrix. Requires - * {@link module:scheduler~MatrixScheduler#setProcessFunction} to be provided - * with a way of processing events. - * @constructor - * @param {module:scheduler~retryAlgorithm} retryAlgorithm Optional. The retry - * algorithm to apply when determining when to try to send an event again. - * Defaults to {@link module:scheduler~MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. - * @param {module:scheduler~queueAlgorithm} queueAlgorithm Optional. The queuing - * algorithm to apply when determining which events should be sent before the - * given event. Defaults to {@link module:scheduler~MatrixScheduler.QUEUE_MESSAGES}. - */ // eslint-disable-next-line camelcase class MatrixScheduler { /** @@ -38,169 +22,173 @@ * times of 2, 4, 8, and 16 seconds (30s total) after which we give up. If the * failure was due to a rate limited request, the time specified in the error is * waited before being retried. - * @param {MatrixEvent} event - * @param {Number} attempts Number of attempts that have been made, including the one that just failed (ie. starting at 1) - * @param {MatrixError} err - * @return {Number} - * @see module:scheduler~retryAlgorithm + * @param attempts - Number of attempts that have been made, including the one that just failed (ie. starting at 1) + * @see retryAlgorithm */ // eslint-disable-next-line @typescript-eslint/naming-convention static RETRY_BACKOFF_RATELIMIT(event, attempts, err) { if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { // client error; no amount of retrying with save you now. return -1; - } // we ship with browser-request which returns { cors: rejected } when trying - // with no connection, so if we match that, give up since they have no conn. - - - if (err["cors"] === "rejected") { + } + if (err instanceof _httpApi.ConnectionError) { return -1; - } // if event that we are trying to send is too large in any way then retrying won't help - + } + // if event that we are trying to send is too large in any way then retrying won't help if (err.name === "M_TOO_LARGE") { return -1; } - if (err.name === "M_LIMIT_EXCEEDED") { const waitTime = err.data.retry_after_ms; - if (waitTime > 0) { return waitTime; } } - if (attempts > 4) { return -1; // give up } return 1000 * Math.pow(2, attempts); } + /** - * Queues m.room.message events and lets other events continue + * Queues `m.room.message` events and lets other events continue * concurrently. - * @param {MatrixEvent} event - * @return {string} - * @see module:scheduler~queueAlgorithm + * @see queueAlgorithm */ // eslint-disable-next-line @typescript-eslint/naming-convention - - static QUEUE_MESSAGES(event) { // enqueue messages or events that associate with another event (redactions and relations) - if (event.getType() === _event.EventType.RoomMessage || event.hasAssocation()) { + if (event.getType() === _event.EventType.RoomMessage || event.hasAssociation()) { // put these events in the 'message' queue. return "message"; - } // allow all other events continue concurrently. - - + } + // allow all other events continue concurrently. return null; - } // queueName: [{ + } + + // queueName: [{ // event: MatrixEvent, // event to send // defer: Deferred, // defer to resolve/reject at the END of the retries // attempts: Number // number of times we've called processFn // }, ...] - - constructor(retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT, queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES) { + /** + * Construct a scheduler for Matrix. Requires + * {@link MatrixScheduler#setProcessFunction} to be provided + * with a way of processing events. + * @param retryAlgorithm - Optional. The retry + * algorithm to apply when determining when to try to send an event again. + * Defaults to {@link MatrixScheduler.RETRY_BACKOFF_RATELIMIT}. + * @param queueAlgorithm - Optional. The queuing + * algorithm to apply when determining which events should be sent before the + * given event. Defaults to {@link MatrixScheduler.QUEUE_MESSAGES}. + */ + constructor( + /** + * The retry algorithm to apply when retrying events. To stop retrying, return + * `-1`. If this event was part of a queue, it will be removed from + * the queue. + * @param event - The event being retried. + * @param attempts - The number of failed attempts. This will always be \>= 1. + * @param err - The most recent error message received when trying + * to send this event. + * @returns The number of milliseconds to wait before trying again. If + * this is 0, the request will be immediately retried. If this is + * `-1`, the event will be marked as + * {@link EventStatus.NOT_SENT} and will not be retried. + */ + retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT, + /** + * The queuing algorithm to apply to events. This function must be idempotent as + * it may be called multiple times with the same event. All queues created are + * serviced in a FIFO manner. To send the event ASAP, return `null` + * which will not put this event in a queue. Events that fail to send that form + * part of a queue will be removed from the queue and the next event in the + * queue will be sent. + * @param event - The event to be sent. + * @returns The name of the queue to put the event into. If a queue with + * this name does not exist, it will be created. If this is `null`, + * the event is not put into a queue and will be sent concurrently. + */ + queueAlgorithm = MatrixScheduler.QUEUE_MESSAGES) { this.retryAlgorithm = retryAlgorithm; this.queueAlgorithm = queueAlgorithm; - _defineProperty(this, "queues", {}); - _defineProperty(this, "activeQueues", []); - _defineProperty(this, "procFn", null); - _defineProperty(this, "processQueue", queueName => { // get head of queue const obj = this.peekNextEvent(queueName); - if (!obj) { - // queue is empty. Mark as inactive and stop recursing. - const index = this.activeQueues.indexOf(queueName); - - if (index >= 0) { - this.activeQueues.splice(index, 1); - } - - debuglog("Stopping queue '%s' as it is now empty", queueName); + this.disableQueue(queueName); return; } - - debuglog("Queue '%s' has %s pending events", queueName, this.queues[queueName].length); // fire the process function and if it resolves, resolve the deferred. Else + debuglog("Queue '%s' has %s pending events", queueName, this.queues[queueName].length); + // fire the process function and if it resolves, resolve the deferred. Else // invoke the retry algorithm. + // First wait for a resolved promise, so the resolve handlers for // the deferred of the previously sent event can run. // This way enqueued relations/redactions to enqueued events can receive // the remove id of their target before being sent. - Promise.resolve().then(() => { return this.procFn(obj.event); }).then(res => { // remove this from the queue this.removeNextEvent(queueName); debuglog("Queue '%s' sent event %s", queueName, obj.event.getId()); - obj.defer.resolve(res); // keep processing - + obj.defer.resolve(res); + // keep processing this.processQueue(queueName); }, err => { - obj.attempts += 1; // ask the retry algorithm when/if we should try again - + obj.attempts += 1; + // ask the retry algorithm when/if we should try again const waitTimeMs = this.retryAlgorithm(obj.event, obj.attempts, err); debuglog("retry(%s) err=%s event_id=%s waitTime=%s", obj.attempts, err, obj.event.getId(), waitTimeMs); - if (waitTimeMs === -1) { // give up (you quitter!) - debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId()); // remove this from the queue - - this.removeNextEvent(queueName); - obj.defer.reject(err); // process next event - - this.processQueue(queueName); + debuglog("Queue '%s' giving up on event %s", queueName, obj.event.getId()); + // remove this from the queue + this.clearQueue(queueName, err); } else { setTimeout(this.processQueue, waitTimeMs, queueName); } }); }); } + /** * Retrieve a queue based on an event. The event provided does not need to be in * the queue. - * @param {MatrixEvent} event An event to get the queue for. - * @return {?Array} A shallow copy of events in the queue or null. + * @param event - An event to get the queue for. + * @returns A shallow copy of events in the queue or null. * Modifying this array will not modify the list itself. Modifying events in * this array will modify the underlying event in the queue. * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. */ - - getQueueForEvent(event) { const name = this.queueAlgorithm(event); - if (!name || !this.queues[name]) { return null; } - return this.queues[name].map(function (obj) { return obj.event; }); } + /** * Remove this event from the queue. The event is equal to another event if they * have the same ID returned from event.getId(). - * @param {MatrixEvent} event The event to remove. - * @return {boolean} True if this event was removed. + * @param event - The event to remove. + * @returns True if this event was removed. */ - - removeEventFromQueue(event) { const name = this.queueAlgorithm(event); - if (!name || !this.queues[name]) { return false; } - let removed = false; utils.removeElement(this.queues[name], element => { if (element.event.getId() === event.getId()) { @@ -209,42 +197,38 @@ removed = true; return true; } + return false; }); return removed; } + /** * Set the process function. Required for events in the queue to be processed. * If set after events have been added to the queue, this will immediately start * processing them. - * @param {module:scheduler~processFn} fn The function that can process events + * @param fn - The function that can process events * in the queue. */ - - setProcessFunction(fn) { this.procFn = fn; this.startProcessingQueues(); } + /** * Queue an event if it is required and start processing queues. - * @param {MatrixEvent} event The event that may be queued. - * @return {?Promise} A promise if the event was queued, which will be + * @param event - The event that may be queued. + * @returns A promise if the event was queued, which will be * resolved or rejected in due time, else null. */ - - queueEvent(event) { const queueName = this.queueAlgorithm(event); - if (!queueName) { return null; - } // add the event to the queue and make a deferred for it. - - + } + // add the event to the queue and make a deferred for it. if (!this.queues[queueName]) { this.queues[queueName] = []; } - const defer = utils.defer(); this.queues[queueName].push({ event: event, @@ -255,83 +239,55 @@ this.startProcessingQueues(); return defer.promise; } - startProcessingQueues() { - if (!this.procFn) return; // for each inactive queue with events in them - + if (!this.procFn) return; + // for each inactive queue with events in them Object.keys(this.queues).filter(queueName => { return this.activeQueues.indexOf(queueName) === -1 && this.queues[queueName].length > 0; }).forEach(queueName => { // mark the queue as active - this.activeQueues.push(queueName); // begin processing the head of the queue - + this.activeQueues.push(queueName); + // begin processing the head of the queue debuglog("Spinning up queue: '%s'", queueName); this.processQueue(queueName); }); } - + disableQueue(queueName) { + // queue is empty. Mark as inactive and stop recursing. + const index = this.activeQueues.indexOf(queueName); + if (index >= 0) { + this.activeQueues.splice(index, 1); + } + debuglog("Stopping queue '%s' as it is now empty", queueName); + } + clearQueue(queueName, err) { + debuglog("clearing queue '%s'", queueName); + let obj; + while (obj = this.removeNextEvent(queueName)) { + obj.defer.reject(err); + } + this.disableQueue(queueName); + } peekNextEvent(queueName) { const queue = this.queues[queueName]; - if (!Array.isArray(queue)) { - return null; + return undefined; } - return queue[0]; } - removeNextEvent(queueName) { const queue = this.queues[queueName]; - if (!Array.isArray(queue)) { - return null; + return undefined; } - return queue.shift(); } - } +/* istanbul ignore next */ exports.MatrixScheduler = MatrixScheduler; - function debuglog(...args) { if (DEBUG) { _logger.logger.log(...args); } -} -/** - * The retry algorithm to apply when retrying events. To stop retrying, return - * -1. If this event was part of a queue, it will be removed from - * the queue. - * @callback retryAlgorithm - * @param {MatrixEvent} event The event being retried. - * @param {Number} attempts The number of failed attempts. This will always be - * >= 1. - * @param {MatrixError} err The most recent error message received when trying - * to send this event. - * @return {Number} The number of milliseconds to wait before trying again. If - * this is 0, the request will be immediately retried. If this is - * -1, the event will be marked as - * {@link module:models/event.EventStatus.NOT_SENT} and will not be retried. - */ - -/** - * The queuing algorithm to apply to events. This function must be idempotent as - * it may be called multiple times with the same event. All queues created are - * serviced in a FIFO manner. To send the event ASAP, return null - * which will not put this event in a queue. Events that fail to send that form - * part of a queue will be removed from the queue and the next event in the - * queue will be sent. - * @callback queueAlgorithm - * @param {MatrixEvent} event The event to be sent. - * @return {string} The name of the queue to put the event into. If a queue with - * this name does not exist, it will be created. If this is null, - * the event is not put into a queue and will be sent concurrently. - */ - -/** - * The function to invoke to process (send) events in the queue. - * @callback processFn - * @param {MatrixEvent} event The event to send. - * @return {Promise} Resolved/rejected depending on the outcome of the request. - */ \ No newline at end of file +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/service-types.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/service-types.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/service-types.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/service-types.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,7 +4,6 @@ value: true }); exports.SERVICE_TYPES = void 0; - /* Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. @@ -20,9 +19,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -let SERVICE_TYPES; +let SERVICE_TYPES; // An integration manager exports.SERVICE_TYPES = SERVICE_TYPES; - (function (SERVICE_TYPES) { SERVICE_TYPES["IS"] = "SERVICE_TYPE_IS"; SERVICE_TYPES["IM"] = "SERVICE_TYPE_IM"; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/sliding-sync.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/sliding-sync.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/sliding-sync.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/sliding-sync.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,117 +3,108 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.SlidingSyncState = exports.SlidingSyncEvent = exports.SlidingSync = exports.ExtensionState = void 0; - +exports.SlidingSyncState = exports.SlidingSyncEvent = exports.SlidingSync = exports.MSC3575_WILDCARD = exports.MSC3575_STATE_KEY_ME = exports.MSC3575_STATE_KEY_LAZY = exports.ExtensionState = void 0; var _logger = require("./logger"); - -var _typedEventEmitter = require("./models//typed-event-emitter"); - +var _typedEventEmitter = require("./models/typed-event-emitter"); var _utils = require("./utils"); - function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } - function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // /sync requests allow you to set a timeout= but the request may continue // beyond that and wedge forever, so we need to track how long we are willing // to keep open the connection. This constant is *ADDED* to the timeout= value // to determine the max time we're willing to wait. const BUFFER_PERIOD_MS = 10 * 1000; +const MSC3575_WILDCARD = "*"; +exports.MSC3575_WILDCARD = MSC3575_WILDCARD; +const MSC3575_STATE_KEY_ME = "$ME"; +exports.MSC3575_STATE_KEY_ME = MSC3575_STATE_KEY_ME; +const MSC3575_STATE_KEY_LAZY = "$LAZY"; + /** * Represents a subscription to a room or set of rooms. Controls which events are returned. */ - +exports.MSC3575_STATE_KEY_LAZY = MSC3575_STATE_KEY_LAZY; let SlidingSyncState; /** * Internal Class. SlidingList represents a single list in sliding sync. The list can have filters, - * multiple sliding windows, and maintains the index->room_id mapping. + * multiple sliding windows, and maintains the index-\>room_id mapping. */ - exports.SlidingSyncState = SlidingSyncState; - (function (SlidingSyncState) { SlidingSyncState["RequestFinished"] = "FINISHED"; SlidingSyncState["Complete"] = "COMPLETE"; })(SlidingSyncState || (exports.SlidingSyncState = SlidingSyncState = {})); - class SlidingList { // returned data /** * Construct a new sliding list. - * @param {MSC3575List} list The range, sort and filter values to use for this list. + * @param list - The range, sort and filter values to use for this list. */ constructor(list) { _defineProperty(this, "list", void 0); - _defineProperty(this, "isModified", void 0); - - _defineProperty(this, "roomIndexToRoomId", void 0); - - _defineProperty(this, "joinedCount", void 0); - + _defineProperty(this, "roomIndexToRoomId", {}); + _defineProperty(this, "joinedCount", 0); this.replaceList(list); } + /** * Mark this list as modified or not. Modified lists will return sticky params with calls to getList. * This is useful for the first time the list is sent, or if the list has changed in some way. - * @param modified True to mark this list as modified so all sticky parameters will be re-sent. + * @param modified - True to mark this list as modified so all sticky parameters will be re-sent. */ - - setModified(modified) { this.isModified = modified; } + /** * Update the list range for this list. Does not affect modified status as list ranges are non-sticky. - * @param newRanges The new ranges for the list + * @param newRanges - The new ranges for the list */ - - updateListRange(newRanges) { this.list.ranges = JSON.parse(JSON.stringify(newRanges)); } + /** * Replace list parameters. All fields will be replaced with the new list parameters. - * @param list The new list parameters + * @param list - The new list parameters */ - - replaceList(list) { list.filters = list.filters || {}; list.ranges = list.ranges || []; this.list = JSON.parse(JSON.stringify(list)); - this.isModified = true; // reset values as the join count may be very different (if filters changed) including the rooms + this.isModified = true; + + // reset values as the join count may be very different (if filters changed) including the rooms // (e.g. sort orders or sliding window ranges changed) + // the constantly changing sliding window ranges. Not an array for performance reasons // E.g. tracking ranges 0-99, 500-599, we don't want to have a 600 element array - - this.roomIndexToRoomId = {}; // the total number of joined rooms according to the server, always >= len(roomIndexToRoomId) - + this.roomIndexToRoomId = {}; + // the total number of joined rooms according to the server, always >= len(roomIndexToRoomId) this.joinedCount = 0; } + /** * Return a copy of the list suitable for a request body. - * @param {boolean} forceIncludeAllParams True to forcibly include all params even if the list + * @param forceIncludeAllParams - True to forcibly include all params even if the list * hasn't been modified. Callers may want to do this if they are modifying the list prior to calling * updateList. */ - - getList(forceIncludeAllParams) { let list = { ranges: JSON.parse(JSON.stringify(this.list.ranges)) }; - if (this.isModified || forceIncludeAllParams) { list = JSON.parse(JSON.stringify(this.list)); } - return list; } + /** * Check if a given index is within the list range. This is required even though the /sync API * provides explicit updates with index positions because of the following situation: @@ -122,39 +113,31 @@ * a b c d _ f COMMAND: DELETE 7; * e a b c d f COMMAND: INSERT 0 e; * c=3 is wrong as we are not tracking it, ergo we need to see if `i` is in range else drop it - * @param i The index to check + * @param i - The index to check * @returns True if the index is within a sliding window */ - - isIndexInRange(i) { for (const r of this.list.ranges) { if (r[0] <= i && i <= r[1]) { return true; } } - return false; } - } + /** * When onResponse extensions should be invoked: before or after processing the main response. */ - - let ExtensionState; /** * An interface that must be satisfied to register extensions */ - exports.ExtensionState = ExtensionState; - (function (ExtensionState) { ExtensionState["PreProcess"] = "ExtState.PreProcess"; ExtensionState["PostProcess"] = "ExtState.PostProcess"; })(ExtensionState || (exports.ExtensionState = ExtensionState = {})); - /** * Events which can be fired by the SlidingSync class. These are designed to provide different levels * of information when processing sync responses. @@ -169,13 +152,11 @@ */ let SlidingSyncEvent; exports.SlidingSyncEvent = SlidingSyncEvent; - (function (SlidingSyncEvent) { SlidingSyncEvent["RoomData"] = "SlidingSync.RoomData"; SlidingSyncEvent["Lifecycle"] = "SlidingSync.Lifecycle"; SlidingSyncEvent["List"] = "SlidingSync.List"; })(SlidingSyncEvent || (exports.SlidingSyncEvent = SlidingSyncEvent = {})); - /** * SlidingSync is a high-level data structure which controls the majority of sliding sync. * It has no hooks into JS SDK except for needing a MatrixClient to perform the HTTP request. @@ -184,19 +165,27 @@ */ class SlidingSync extends _typedEventEmitter.TypedEventEmitter { // flag set when resend() is called because we cannot rely on detecting AbortError in JS SDK :( + // the txn_id to send with the next request. + // a list (in chronological order of when they were sent) of objects containing the txn ID and // a defer to resolve/reject depending on whether they were successfully sent or not. + // map of extension name to req/resp handler + // the *desired* room subscriptions + // map of custom subscription name to the subscription + + // map of room ID to custom subscription name + /** * Create a new sliding sync instance - * @param {string} proxyBaseUrl The base URL of the sliding sync proxy - * @param {MSC3575List[]} lists The lists to use for sliding sync. - * @param {MSC3575RoomSubscription} roomSubscriptionInfo The params to use for room subscriptions. - * @param {MatrixClient} client The client to use for /sync calls. - * @param {number} timeoutMS The number of milliseconds to wait for a response. + * @param proxyBaseUrl - The base URL of the sliding sync proxy + * @param lists - The lists to use for sliding sync. + * @param roomSubscriptionInfo - The params to use for room subscriptions. + * @param client - The client to use for /sync calls. + * @param timeoutMS - The number of milliseconds to wait for a response. */ constructor(proxyBaseUrl, lists, roomSubscriptionInfo, client, timeoutMS) { super(); @@ -204,160 +193,174 @@ this.roomSubscriptionInfo = roomSubscriptionInfo; this.client = client; this.timeoutMS = timeoutMS; - _defineProperty(this, "lists", void 0); - _defineProperty(this, "listModifiedCount", 0); - _defineProperty(this, "terminated", false); - _defineProperty(this, "needsResend", false); - _defineProperty(this, "txnId", null); - _defineProperty(this, "txnIdDefers", []); - _defineProperty(this, "extensions", {}); - _defineProperty(this, "desiredRoomSubscriptions", new Set()); - _defineProperty(this, "confirmedRoomSubscriptions", new Set()); - + _defineProperty(this, "customSubscriptions", new Map()); + _defineProperty(this, "roomIdToCustomSubscription", new Map()); _defineProperty(this, "pendingReq", void 0); + _defineProperty(this, "abortController", void 0); + this.lists = new Map(); + lists.forEach((list, key) => { + this.lists.set(key, new SlidingList(list)); + }); + } - this.lists = lists.map(l => new SlidingList(l)); + /** + * Add a custom room subscription, referred to by an arbitrary name. If a subscription with this + * name already exists, it is replaced. No requests are sent by calling this method. + * @param name - The name of the subscription. Only used to reference this subscription in + * useCustomSubscription. + * @param sub - The subscription information. + */ + addCustomSubscription(name, sub) { + if (this.customSubscriptions.has(name)) { + _logger.logger.warn(`addCustomSubscription: ${name} already exists as a custom subscription, ignoring.`); + return; + } + this.customSubscriptions.set(name, sub); } + /** - * Get the length of the sliding lists. - * @returns The number of lists in the sync request + * Use a custom subscription previously added via addCustomSubscription. No requests are sent + * by calling this method. Use modifyRoomSubscriptions to resend subscription information. + * @param roomId - The room to use the subscription in. + * @param name - The name of the subscription. If this name is unknown, the default subscription + * will be used. */ - - - listLength() { - return this.lists.length; + useCustomSubscription(roomId, name) { + // We already know about this custom subscription, as it is immutable, + // we don't need to unconfirm the subscription. + if (this.roomIdToCustomSubscription.get(roomId) === name) { + return; + } + this.roomIdToCustomSubscription.set(roomId, name); + // unconfirm this subscription so a resend() will send it up afresh. + this.confirmedRoomSubscriptions.delete(roomId); } + /** - * Get the room data for a list. - * @param index The list index + * Get the room index data for a list. + * @param key - The list key * @returns The list data which contains the rooms in this list */ - - - getListData(index) { - if (!this.lists[index]) { + getListData(key) { + const data = this.lists.get(key); + if (!data) { return null; } - return { - joinedCount: this.lists[index].joinedCount, - roomIndexToRoomId: Object.assign({}, this.lists[index].roomIndexToRoomId) + joinedCount: data.joinedCount, + roomIndexToRoomId: Object.assign({}, data.roomIndexToRoomId) }; } + /** - * Get the full list parameters for a list index. This function is provided for callers to use + * Get the full request list parameters for a list index. This function is provided for callers to use * in conjunction with setList to update fields on an existing list. - * @param index The list index to get the list for. - * @returns A copy of the list or undefined. + * @param key - The list key to get the params for. + * @returns A copy of the list params or undefined. */ - - - getList(index) { - if (!this.lists[index]) { + getListParams(key) { + const params = this.lists.get(key); + if (!params) { return null; } - - return this.lists[index].getList(true); + return params.getList(true); } + /** * Set new ranges for an existing list. Calling this function when _only_ the ranges have changed * is more efficient than calling setList(index,list) as this function won't resend sticky params, * whereas setList always will. - * @param index The list index to modify - * @param ranges The new ranges to apply. - * @return A promise which resolves to the transaction ID when it has been received down sync + * @param key - The list key to modify + * @param ranges - The new ranges to apply. + * @returns A promise which resolves to the transaction ID when it has been received down sync * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled * immediately after sending, in which case the action will be applied in the subsequent request) */ - - - setListRanges(index, ranges) { - this.lists[index].updateListRange(ranges); + setListRanges(key, ranges) { + const list = this.lists.get(key); + if (!list) { + return Promise.reject(new Error("no list with key " + key)); + } + list.updateListRange(ranges); return this.resend(); } + /** * Add or replace a list. Calling this function will interrupt the /sync request to resend new * lists. - * @param index The index to modify - * @param list The new list parameters. - * @return A promise which resolves to the transaction ID when it has been received down sync + * @param key - The key to modify + * @param list - The new list parameters. + * @returns A promise which resolves to the transaction ID when it has been received down sync * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled * immediately after sending, in which case the action will be applied in the subsequent request) */ - - - setList(index, list) { - if (this.lists[index]) { - this.lists[index].replaceList(list); + setList(key, list) { + const existingList = this.lists.get(key); + if (existingList) { + existingList.replaceList(list); + this.lists.set(key, existingList); } else { - this.lists[index] = new SlidingList(list); + this.lists.set(key, new SlidingList(list)); } - this.listModifiedCount += 1; return this.resend(); } + /** * Get the room subscriptions for the sync API. * @returns A copy of the desired room subscriptions. */ - - getRoomSubscriptions() { return new Set(Array.from(this.desiredRoomSubscriptions)); } + /** * Modify the room subscriptions for the sync API. Calling this function will interrupt the * /sync request to resend new subscriptions. If the /sync stream has not started, this will * prepare the room subscriptions for when start() is called. - * @param s The new desired room subscriptions. - * @return A promise which resolves to the transaction ID when it has been received down sync + * @param s - The new desired room subscriptions. + * @returns A promise which resolves to the transaction ID when it has been received down sync * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled * immediately after sending, in which case the action will be applied in the subsequent request) */ - - modifyRoomSubscriptions(s) { this.desiredRoomSubscriptions = s; return this.resend(); } + /** * Modify which events to retrieve for room subscriptions. Invalidates all room subscriptions * such that they will be sent up afresh. - * @param rs The new room subscription fields to fetch. - * @return A promise which resolves to the transaction ID when it has been received down sync + * @param rs - The new room subscription fields to fetch. + * @returns A promise which resolves to the transaction ID when it has been received down sync * (or rejects with the transaction ID if the action was not applied e.g the request was cancelled * immediately after sending, in which case the action will be applied in the subsequent request) */ - - modifyRoomSubscriptionInfo(rs) { this.roomSubscriptionInfo = rs; this.confirmedRoomSubscriptions = new Set(); return this.resend(); } + /** * Register an extension to send with the /sync request. - * @param ext The extension to register. + * @param ext - The extension to register. */ - - registerExtension(ext) { if (this.extensions[ext.name()]) { throw new Error(`registerExtension: ${ext.name()} already exists as an extension`); } - this.extensions[ext.name()] = ext; } - getExtensionRequest(isInitial) { const ext = {}; Object.keys(this.extensions).forEach(extName => { @@ -365,7 +368,6 @@ }); return ext; } - onPreExtensionsResponse(ext) { Object.keys(ext).forEach(extName => { if (this.extensions[extName].when() == ExtensionState.PreProcess) { @@ -373,7 +375,6 @@ } }); } - onPostExtensionsResponse(ext) { Object.keys(ext).forEach(extName => { if (this.extensions[extName].when() == ExtensionState.PostProcess) { @@ -381,123 +382,126 @@ } }); } + /** * Invoke all attached room data listeners. - * @param {string} roomId The room which received some data. - * @param {object} roomData The raw sliding sync response JSON. + * @param roomId - The room which received some data. + * @param roomData - The raw sliding sync response JSON. */ - - invokeRoomDataListeners(roomId, roomData) { if (!roomData.required_state) { roomData.required_state = []; } - if (!roomData.timeline) { roomData.timeline = []; } - this.emit(SlidingSyncEvent.RoomData, roomId, roomData); } + /** * Invoke all attached lifecycle listeners. - * @param {SlidingSyncState} state The Lifecycle state - * @param {object} resp The raw sync response JSON - * @param {Error?} err Any error that occurred when making the request e.g. network errors. + * @param state - The Lifecycle state + * @param resp - The raw sync response JSON + * @param err - Any error that occurred when making the request e.g. network errors. */ - - invokeLifecycleListeners(state, resp, err) { this.emit(SlidingSyncEvent.Lifecycle, state, resp, err); } - - shiftRight(listIndex, hi, low) { + shiftRight(listKey, hi, low) { + const list = this.lists.get(listKey); + if (!list) { + return; + } // l h // 0,1,2,3,4 <- before // 0,1,2,2,3 <- after, hi is deleted and low is duplicated for (let i = hi; i > low; i--) { - if (this.lists[listIndex].isIndexInRange(i)) { - this.lists[listIndex].roomIndexToRoomId[i] = this.lists[listIndex].roomIndexToRoomId[i - 1]; + if (list.isIndexInRange(i)) { + list.roomIndexToRoomId[i] = list.roomIndexToRoomId[i - 1]; } } } - - shiftLeft(listIndex, hi, low) { + shiftLeft(listKey, hi, low) { + const list = this.lists.get(listKey); + if (!list) { + return; + } // l h // 0,1,2,3,4 <- before // 0,1,3,4,4 <- after, low is deleted and hi is duplicated for (let i = low; i < hi; i++) { - if (this.lists[listIndex].isIndexInRange(i)) { - this.lists[listIndex].roomIndexToRoomId[i] = this.lists[listIndex].roomIndexToRoomId[i + 1]; + if (list.isIndexInRange(i)) { + list.roomIndexToRoomId[i] = list.roomIndexToRoomId[i + 1]; } } } - - removeEntry(listIndex, index) { + removeEntry(listKey, index) { + const list = this.lists.get(listKey); + if (!list) { + return; + } // work out the max index let max = -1; - - for (const n in this.lists[listIndex].roomIndexToRoomId) { + for (const n in list.roomIndexToRoomId) { if (Number(n) > max) { max = Number(n); } } - if (max < 0 || index > max) { return; - } // Everything higher than the gap needs to be shifted left. - - - this.shiftLeft(listIndex, max, index); - delete this.lists[listIndex].roomIndexToRoomId[max]; - } - - addEntry(listIndex, index) { + } + // Everything higher than the gap needs to be shifted left. + this.shiftLeft(listKey, max, index); + delete list.roomIndexToRoomId[max]; + } + addEntry(listKey, index) { + const list = this.lists.get(listKey); + if (!list) { + return; + } // work out the max index let max = -1; - - for (const n in this.lists[listIndex].roomIndexToRoomId) { + for (const n in list.roomIndexToRoomId) { if (Number(n) > max) { max = Number(n); } } - if (max < 0 || index > max) { return; - } // Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element - - - this.shiftRight(listIndex, max + 1, index); + } + // Everything higher than the gap needs to be shifted right, +1 so we don't delete the highest element + this.shiftRight(listKey, max + 1, index); } - - processListOps(list, listIndex) { + processListOps(list, listKey) { let gapIndex = -1; + const listData = this.lists.get(listKey); + if (!listData) { + return; + } list.ops.forEach(op => { + if (!listData) { + return; + } switch (op.op) { case "DELETE": { - _logger.logger.debug("DELETE", listIndex, op.index, ";"); - - delete this.lists[listIndex].roomIndexToRoomId[op.index]; - + _logger.logger.debug("DELETE", listKey, op.index, ";"); + delete listData.roomIndexToRoomId[op.index]; if (gapIndex !== -1) { // we already have a DELETE operation to process, so process it. - this.removeEntry(listIndex, gapIndex); + this.removeEntry(listKey, gapIndex); } - gapIndex = op.index; break; } - case "INSERT": { - _logger.logger.debug("INSERT", listIndex, op.index, op.room_id, ";"); - - if (this.lists[listIndex].roomIndexToRoomId[op.index]) { + _logger.logger.debug("INSERT", listKey, op.index, op.room_id, ";"); + if (listData.roomIndexToRoomId[op.index]) { // something is in this space, shift items out of the way if (gapIndex < 0) { // we haven't been told where to shift from, so make way for a new room entry. - this.addEntry(listIndex, op.index); + this.addEntry(listKey, op.index); } else if (gapIndex > op.index) { // the gap is further down the list, shift every element to the right // starting at the gap so we can just shift each element in turn: @@ -506,267 +510,264 @@ // [A,B,B,C] i=2 // [A,A,B,C] i=1 // Terminate. We'll assign into op.index next. - this.shiftRight(listIndex, gapIndex, op.index); + this.shiftRight(listKey, gapIndex, op.index); } else if (gapIndex < op.index) { // the gap is further up the list, shift every element to the left // starting at the gap so we can just shift each element in turn - this.shiftLeft(listIndex, op.index, gapIndex); + this.shiftLeft(listKey, op.index, gapIndex); } - - gapIndex = -1; // forget the gap, we don't need it anymore. } - - this.lists[listIndex].roomIndexToRoomId[op.index] = op.room_id; + // forget the gap, we don't need it anymore. This is outside the check for + // a room being present in this index position because INSERTs always universally + // forget the gap, not conditionally based on the presence of a room in the INSERT + // position. Without this, DELETE 0; INSERT 0; would do the wrong thing. + gapIndex = -1; + listData.roomIndexToRoomId[op.index] = op.room_id; break; } - case "INVALIDATE": { const startIndex = op.range[0]; - for (let i = startIndex; i <= op.range[1]; i++) { - delete this.lists[listIndex].roomIndexToRoomId[i]; + delete listData.roomIndexToRoomId[i]; } - - _logger.logger.debug("INVALIDATE", listIndex, op.range[0], op.range[1], ";"); - + _logger.logger.debug("INVALIDATE", listKey, op.range[0], op.range[1], ";"); break; } - case "SYNC": { const startIndex = op.range[0]; - for (let i = startIndex; i <= op.range[1]; i++) { const roomId = op.room_ids[i - startIndex]; - if (!roomId) { break; // we are at the end of list } - this.lists[listIndex].roomIndexToRoomId[i] = roomId; + listData.roomIndexToRoomId[i] = roomId; } - - _logger.logger.debug("SYNC", listIndex, op.range[0], op.range[1], (op.room_ids || []).join(" "), ";"); - + _logger.logger.debug("SYNC", listKey, op.range[0], op.range[1], (op.room_ids || []).join(" "), ";"); break; } } }); - if (gapIndex !== -1) { // we already have a DELETE operation to process, so process it // Everything higher than the gap needs to be shifted left. - this.removeEntry(listIndex, gapIndex); + this.removeEntry(listKey, gapIndex); } } + /** * Resend a Sliding Sync request. Used when something has changed in the request. Resolves with * the transaction ID of this request on success. Rejects with the transaction ID of this request * on failure. */ - - resend() { if (this.needsResend && this.txnIdDefers.length > 0) { // we already have a resend queued, so just return the same promise return this.txnIdDefers[this.txnIdDefers.length - 1].promise; } - this.needsResend = true; this.txnId = this.client.makeTxnId(); const d = (0, _utils.defer)(); this.txnIdDefers.push(_objectSpread(_objectSpread({}, d), {}, { txnId: this.txnId })); - this.pendingReq?.abort(); + this.abortController?.abort(); + this.abortController = new AbortController(); return d.promise; } - resolveTransactionDefers(txnId) { if (!txnId) { return; - } // find the matching index - - + } + // find the matching index let txnIndex = -1; - for (let i = 0; i < this.txnIdDefers.length; i++) { if (this.txnIdDefers[i].txnId === txnId) { txnIndex = i; break; } } - if (txnIndex === -1) { // this shouldn't happen; we shouldn't be seeing txn_ids for things we don't know about, // whine about it. _logger.logger.warn(`resolveTransactionDefers: seen ${txnId} but it isn't a pending txn, ignoring.`); - return; - } // This list is sorted in time, so if the input txnId ACKs in the middle of this array, + } + // This list is sorted in time, so if the input txnId ACKs in the middle of this array, // then everything before it that hasn't been ACKed yet never will and we should reject them. - - for (let i = 0; i < txnIndex; i++) { this.txnIdDefers[i].reject(this.txnIdDefers[i].txnId); } - - this.txnIdDefers[txnIndex].resolve(txnId); // clear out settled promises, incuding the one we resolved. - + this.txnIdDefers[txnIndex].resolve(txnId); + // clear out settled promises, including the one we resolved. this.txnIdDefers = this.txnIdDefers.slice(txnIndex + 1); } + /** * Stop syncing with the server. */ - - stop() { this.terminated = true; - this.pendingReq?.abort(); // remove all listeners so things can be GC'd - + this.abortController?.abort(); + // remove all listeners so things can be GC'd this.removeAllListeners(SlidingSyncEvent.Lifecycle); this.removeAllListeners(SlidingSyncEvent.List); this.removeAllListeners(SlidingSyncEvent.RoomData); } + /** - * Start syncing with the server. Blocks until stopped. + * Re-setup this connection e.g in the event of an expired session. */ + resetup() { + _logger.logger.warn("SlidingSync: resetting connection info"); + // any pending txn ID defers will be forgotten already by the server, so clear them out + this.txnIdDefers.forEach(d => { + d.reject(d.txnId); + }); + this.txnIdDefers = []; + // resend sticky params and de-confirm all subscriptions + this.lists.forEach(l => { + l.setModified(true); + }); + this.confirmedRoomSubscriptions = new Set(); // leave desired ones alone though! + // reset the connection as we might be wedged + this.needsResend = true; + this.abortController?.abort(); + this.abortController = new AbortController(); + } - + /** + * Start syncing with the server. Blocks until stopped. + */ async start() { + this.abortController = new AbortController(); let currentPos; - while (!this.terminated) { this.needsResend = false; let doNotUpdateList = false; let resp; - try { const listModifiedCount = this.listModifiedCount; + const reqLists = {}; + this.lists.forEach((l, key) => { + reqLists[key] = l.getList(false); + }); const reqBody = { - lists: this.lists.map(l => { - return l.getList(false); - }), + lists: reqLists, pos: currentPos, timeout: this.timeoutMS, clientTimeout: this.timeoutMS + BUFFER_PERIOD_MS, extensions: this.getExtensionRequest(currentPos === undefined) - }; // check if we are (un)subscribing to a room and modify request this one time for it - + }; + // check if we are (un)subscribing to a room and modify request this one time for it const newSubscriptions = difference(this.desiredRoomSubscriptions, this.confirmedRoomSubscriptions); const unsubscriptions = difference(this.confirmedRoomSubscriptions, this.desiredRoomSubscriptions); - if (unsubscriptions.size > 0) { reqBody.unsubscribe_rooms = Array.from(unsubscriptions); } - if (newSubscriptions.size > 0) { reqBody.room_subscriptions = {}; - for (const roomId of newSubscriptions) { - reqBody.room_subscriptions[roomId] = this.roomSubscriptionInfo; + const customSubName = this.roomIdToCustomSubscription.get(roomId); + let sub = this.roomSubscriptionInfo; + if (customSubName && this.customSubscriptions.has(customSubName)) { + sub = this.customSubscriptions.get(customSubName); + } + reqBody.room_subscriptions[roomId] = sub; } } - if (this.txnId) { reqBody.txn_id = this.txnId; this.txnId = null; } - - this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl); + this.pendingReq = this.client.slidingSync(reqBody, this.proxyBaseUrl, this.abortController.signal); resp = await this.pendingReq; - - _logger.logger.debug(resp); - - currentPos = resp.pos; // update what we think we're subscribed to. - + currentPos = resp.pos; + // update what we think we're subscribed to. for (const roomId of newSubscriptions) { this.confirmedRoomSubscriptions.add(roomId); } - for (const roomId of unsubscriptions) { this.confirmedRoomSubscriptions.delete(roomId); } - if (listModifiedCount !== this.listModifiedCount) { // the lists have been modified whilst we were waiting for 'await' to return, but the abort() // call did nothing. It is NOT SAFE to modify the list array now. We'll process the response but // not update list pointers. _logger.logger.debug("list modified during await call, not updating list"); - doNotUpdateList = true; - } // mark all these lists as having been sent as sticky so we don't keep sending sticky params - - + } + // mark all these lists as having been sent as sticky so we don't keep sending sticky params this.lists.forEach(l => { l.setModified(false); - }); // set default empty values so we don't need to null check - - resp.lists = resp.lists || []; + }); + // set default empty values so we don't need to null check + resp.lists = resp.lists || {}; resp.rooms = resp.rooms || {}; resp.extensions = resp.extensions || {}; - resp.lists.forEach((val, i) => { - this.lists[i].joinedCount = val.count; + Object.keys(resp.lists).forEach(key => { + const list = this.lists.get(key); + if (!list || !resp) { + return; + } + list.joinedCount = resp.lists[key].count; }); this.invokeLifecycleListeners(SlidingSyncState.RequestFinished, resp); } catch (err) { if (err.httpStatus) { this.invokeLifecycleListeners(SlidingSyncState.RequestFinished, null, err); - await (0, _utils.sleep)(3000); - } else if (this.needsResend || err === "aborted") { - // don't sleep as we caused this error by abort()ing the request. - // we check for 'aborted' because that's the error Jest returns and without it - // we get warnings about not exiting fast enough. - continue; - } else { - _logger.logger.error(err); - - await (0, _utils.sleep)(3000); + if (err.httpStatus === 400) { + // session probably expired TODO: assign an errcode + // so drop state and re-request + this.resetup(); + currentPos = undefined; + await (0, _utils.sleep)(50); // in case the 400 was for something else; don't tightloop + continue; + } // else fallthrough to generic error handling + } else if (this.needsResend || err.name === "AbortError") { + continue; // don't sleep as we caused this error by abort()ing the request. } - } + _logger.logger.error(err); + await (0, _utils.sleep)(5000); + } if (!resp) { continue; } - this.onPreExtensionsResponse(resp.extensions); Object.keys(resp.rooms).forEach(roomId => { this.invokeRoomDataListeners(roomId, resp.rooms[roomId]); }); - const listIndexesWithUpdates = new Set(); - + const listKeysWithUpdates = new Set(); if (!doNotUpdateList) { - resp.lists.forEach((list, listIndex) => { + for (const [key, list] of Object.entries(resp.lists)) { list.ops = list.ops || []; - if (list.ops.length > 0) { - listIndexesWithUpdates.add(listIndex); + listKeysWithUpdates.add(key); } - - this.processListOps(list, listIndex); - }); + this.processListOps(list, key); + } } - this.invokeLifecycleListeners(SlidingSyncState.Complete, resp); this.onPostExtensionsResponse(resp.extensions); - listIndexesWithUpdates.forEach(i => { - this.emit(SlidingSyncEvent.List, i, this.lists[i].joinedCount, Object.assign({}, this.lists[i].roomIndexToRoomId)); + listKeysWithUpdates.forEach(listKey => { + const list = this.lists.get(listKey); + if (!list) { + return; + } + this.emit(SlidingSyncEvent.List, listKey, list.joinedCount, Object.assign({}, list.roomIndexToRoomId)); }); this.resolveTransactionDefers(resp.txn_id); } } - } - exports.SlidingSync = SlidingSync; - const difference = (setA, setB) => { const diff = new Set(setA); - for (const elem of setB) { diff.delete(elem); } - return diff; }; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/sliding-sync-sdk.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/sliding-sync-sdk.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/sliding-sync-sdk.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/sliding-sync-sdk.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,59 +4,42 @@ value: true }); exports.SlidingSyncSdk = void 0; - var _room = require("./models/room"); - var _logger = require("./logger"); - var utils = _interopRequireWildcard(require("./utils")); - var _eventTimeline = require("./models/event-timeline"); - var _client = require("./client"); - var _sync = require("./sync"); - var _httpApi = require("./http-api"); - var _slidingSync = require("./sliding-sync"); - -var _matrix = require("./matrix"); - -var _pushprocessor = require("./pushprocessor"); - +var _event = require("./@types/event"); +var _roomState = require("./models/room-state"); +var _roomMember = require("./models/room-member"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed // to RECONNECTING. This is needed to inform the client of server issues when the // keepAlive is successful but the server /sync fails. const FAILED_SYNC_ERROR_THRESHOLD = 3; - class ExtensionE2EE { constructor(crypto) { this.crypto = crypto; } - name() { return "e2ee"; } - when() { return _slidingSync.ExtensionState.PreProcess; } - onRequest(isInitial) { if (!isInitial) { return undefined; } - return { enabled: true // this is sticky so only send it on the initial request - }; } @@ -65,59 +48,53 @@ if (data["device_lists"]) { await this.crypto.handleDeviceListChanges({ oldSyncToken: "yep" // XXX need to do this so the device list changes get processed :( - }, data["device_lists"]); - } // Handle one_time_keys_count - + } + // Handle one_time_keys_count if (data["device_one_time_keys_count"]) { const currentCount = data["device_one_time_keys_count"].signed_curve25519 || 0; this.crypto.updateOneTimeKeyCount(currentCount); } - if (data["device_unused_fallback_key_types"] || data["org.matrix.msc2732.device_unused_fallback_key_types"]) { // The presence of device_unused_fallback_key_types indicates that the // server supports fallback keys. If there's no unused // signed_curve25519 fallback key we need a new one. const unusedFallbackKeys = data["device_unused_fallback_key_types"] || data["org.matrix.msc2732.device_unused_fallback_key_types"]; - this.crypto.setNeedsNewFallback(unusedFallbackKeys instanceof Array && !unusedFallbackKeys.includes("signed_curve25519")); + this.crypto.setNeedsNewFallback(Array.isArray(unusedFallbackKeys) && !unusedFallbackKeys.includes("signed_curve25519")); } + this.crypto.onSyncCompleted({}); } - } - class ExtensionToDevice { - constructor(client) { + constructor(client, cryptoCallbacks) { this.client = client; - + this.cryptoCallbacks = cryptoCallbacks; _defineProperty(this, "nextBatch", null); } - name() { return "to_device"; } - when() { return _slidingSync.ExtensionState.PreProcess; } - onRequest(isInitial) { const extReq = { since: this.nextBatch !== null ? this.nextBatch : undefined }; - if (isInitial) { extReq["limit"] = 100; extReq["enabled"] = true; } - return extReq; } - async onResponse(data) { const cancelledKeyVerificationTxns = []; - data["events"] = data["events"] || []; - data["events"].map(this.client.getEventMapper()).map(toDeviceEvent => { + let events = data["events"] || []; + if (events.length > 0 && this.cryptoCallbacks) { + events = await this.cryptoCallbacks.preprocessToDeviceMessages(events); + } + events.map(this.client.getEventMapper()).map(toDeviceEvent => { // map is a cheap inline forEach // We want to flag m.key.verification.start events as cancelled // if there's an accompanying m.key.verification.cancel event, so @@ -125,90 +102,72 @@ // so we can flag the verification events as cancelled in the loop // below. if (toDeviceEvent.getType() === "m.key.verification.cancel") { - const txnId = toDeviceEvent.getContent()['transaction_id']; - + const txnId = toDeviceEvent.getContent()["transaction_id"]; if (txnId) { cancelledKeyVerificationTxns.push(txnId); } - } // as mentioned above, .map is a cheap inline forEach, so return - // the unmodified event. - + } + // as mentioned above, .map is a cheap inline forEach, so return + // the unmodified event. return toDeviceEvent; }).forEach(toDeviceEvent => { const content = toDeviceEvent.getContent(); - if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") { // the mapper already logged a warning. - _logger.logger.log('Ignoring undecryptable to-device event from ' + toDeviceEvent.getSender()); - + _logger.logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender()); return; } - if (toDeviceEvent.getType() === "m.key.verification.start" || toDeviceEvent.getType() === "m.key.verification.request") { - const txnId = content['transaction_id']; - + const txnId = content["transaction_id"]; if (cancelledKeyVerificationTxns.includes(txnId)) { toDeviceEvent.flagCancelled(); } } - this.client.emit(_client.ClientEvent.ToDeviceEvent, toDeviceEvent); }); - this.nextBatch = data["next_batch"]; + this.nextBatch = data.next_batch; } - } - class ExtensionAccountData { constructor(client) { this.client = client; } - name() { return "account_data"; } - when() { return _slidingSync.ExtensionState.PostProcess; } - onRequest(isInitial) { if (!isInitial) { return undefined; } - return { enabled: true }; } - onResponse(data) { if (data.global && data.global.length > 0) { this.processGlobalAccountData(data.global); } - for (const roomId in data.rooms) { const accountDataEvents = mapEvents(this.client, roomId, data.rooms[roomId]); const room = this.client.getRoom(roomId); - if (!room) { _logger.logger.warn("got account data for room but room doesn't exist on client:", roomId); - continue; } - room.addAccountData(accountDataEvents); accountDataEvents.forEach(e => { this.client.emit(_client.ClientEvent.Event, e); }); } } - processGlobalAccountData(globalAccountData) { const events = mapEvents(this.client, undefined, globalAccountData); const prevEventsMap = events.reduce((m, c) => { - m[c.getId()] = this.client.store.getAccountData(c.getType()); + m[c.getType()] = this.client.store.getAccountData(c.getType()); return m; }, {}); this.client.store.storeAccountDataEvents(events); @@ -217,111 +176,137 @@ // honour push rules that were previously cached. Base rules // will be updated when we receive push rules via getPushRules // (see sync) before syncing over the network. - if (accountDataEvent.getType() === _matrix.EventType.PushRules) { + if (accountDataEvent.getType() === _event.EventType.PushRules) { const rules = accountDataEvent.getContent(); - this.client.pushRules = _pushprocessor.PushProcessor.rewriteDefaultRules(rules); + this.client.setPushRules(rules); } - - const prevEvent = prevEventsMap[accountDataEvent.getId()]; + const prevEvent = prevEventsMap[accountDataEvent.getType()]; this.client.emit(_client.ClientEvent.AccountData, accountDataEvent, prevEvent); return accountDataEvent; }); } +} +class ExtensionTyping { + constructor(client) { + this.client = client; + } + name() { + return "typing"; + } + when() { + return _slidingSync.ExtensionState.PostProcess; + } + onRequest(isInitial) { + if (!isInitial) { + return undefined; // don't send a JSON object for subsequent requests, we don't need to. + } + return { + enabled: true + }; + } + onResponse(data) { + if (!data?.rooms) { + return; + } + for (const roomId in data.rooms) { + processEphemeralEvents(this.client, roomId, [data.rooms[roomId]]); + } + } } +class ExtensionReceipts { + constructor(client) { + this.client = client; + } + name() { + return "receipts"; + } + when() { + return _slidingSync.ExtensionState.PostProcess; + } + onRequest(isInitial) { + if (isInitial) { + return { + enabled: true + }; + } + return undefined; // don't send a JSON object for subsequent requests, we don't need to. + } + + onResponse(data) { + if (!data?.rooms) { + return; + } + for (const roomId in data.rooms) { + processEphemeralEvents(this.client, roomId, [data.rooms[roomId]]); + } + } +} + /** * A copy of SyncApi such that it can be used as a drop-in replacement for sync v2. For the actual * sliding sync API, see sliding-sync.ts or the class SlidingSync. */ - - class SlidingSyncSdk { // accumulator of sync events in the current sync response - constructor(slidingSync, client, opts = {}) { + + constructor(slidingSync, client, opts, syncOpts) { this.slidingSync = slidingSync; this.client = client; - this.opts = opts; - + _defineProperty(this, "opts", void 0); + _defineProperty(this, "syncOpts", void 0); _defineProperty(this, "syncState", null); - _defineProperty(this, "syncStateData", void 0); - _defineProperty(this, "lastPos", null); - _defineProperty(this, "failCount", 0); - _defineProperty(this, "notifEvents", []); - - this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8; - this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false; - this.opts.pollTimeout = this.opts.pollTimeout || 30 * 1000; - this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || _client.PendingEventOrdering.Chronological; - this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true; - - if (!opts.canResetEntireTimeline) { - opts.canResetEntireTimeline = _roomId => { - return false; - }; - } - + this.opts = (0, _sync.defaultClientOpts)(opts); + this.syncOpts = (0, _sync.defaultSyncApiOpts)(syncOpts); if (client.getNotifTimelineSet()) { client.reEmitter.reEmit(client.getNotifTimelineSet(), [_room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset]); } - this.slidingSync.on(_slidingSync.SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this)); this.slidingSync.on(_slidingSync.SlidingSyncEvent.RoomData, this.onRoomData.bind(this)); - const extensions = [new ExtensionToDevice(this.client), new ExtensionAccountData(this.client)]; - - if (this.opts.crypto) { - extensions.push(new ExtensionE2EE(this.opts.crypto)); + const extensions = [new ExtensionToDevice(this.client, this.syncOpts.cryptoCallbacks), new ExtensionAccountData(this.client), new ExtensionTyping(this.client), new ExtensionReceipts(this.client)]; + if (this.syncOpts.crypto) { + extensions.push(new ExtensionE2EE(this.syncOpts.crypto)); } - extensions.forEach(ext => { this.slidingSync.registerExtension(ext); }); } - onRoomData(roomId, roomData) { let room = this.client.store.getRoom(roomId); - if (!room) { if (!roomData.initial) { _logger.logger.debug("initial flag not set but no stored room exists for room ", roomId, roomData); - return; } - room = (0, _sync._createAndReEmitRoom)(this.client, roomId, this.opts); } - this.processRoomData(this.client, room, roomData); } - onLifecycle(state, resp, err) { if (err) { _logger.logger.debug("onLifecycle", state, err); } - switch (state) { case _slidingSync.SlidingSyncState.Complete: this.purgeNotifications(); - if (!resp) { break; - } // Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared - - + } + // Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared if (!this.lastPos) { this.updateSyncState(_sync.SyncState.Prepared, { - oldSyncToken: this.lastPos, + oldSyncToken: undefined, nextSyncToken: resp.pos, catchingUp: false, fromCache: false }); - } // Conversely, Element won't show the room list unless there is at least 1x SyncState.Syncing + } + // Conversely, Element won't show the room list unless there is at least 1x SyncState.Syncing // so hence for the very first sync we will fire prepared then immediately syncing. - - this.updateSyncState(_sync.SyncState.Syncing, { oldSyncToken: this.lastPos, nextSyncToken: resp.pos, @@ -330,101 +315,126 @@ }); this.lastPos = resp.pos; break; - case _slidingSync.SlidingSyncState.RequestFinished: if (err) { this.failCount += 1; this.updateSyncState(this.failCount > FAILED_SYNC_ERROR_THRESHOLD ? _sync.SyncState.Error : _sync.SyncState.Reconnecting, { error: new _httpApi.MatrixError(err) }); - if (this.shouldAbortSync(new _httpApi.MatrixError(err))) { return; // shouldAbortSync actually stops syncing too so we don't need to do anything. } } else { this.failCount = 0; } - break; } } + /** * Sync rooms the user has left. - * @return {Promise} Resolved when they've been added to the store. + * @returns Resolved when they've been added to the store. */ - - async syncLeftRooms() { return []; // TODO } + /** * Peek into a room. This will result in the room in question being synced so it * is accessible via getRooms(). Live updates for the room will be provided. - * @param {string} roomId The room ID to peek into. - * @return {Promise} A promise which resolves once the room has been added to the + * @param roomId - The room ID to peek into. + * @returns A promise which resolves once the room has been added to the * store. */ - - async peek(_roomId) { return null; // TODO } + /** * Stop polling for updates in the peeked room. NOPs if there is no room being * peeked. */ - - - stopPeeking() {// TODO + stopPeeking() { + // TODO } + /** * Returns the current state of this sync object - * @see module:client~MatrixClient#event:"sync" - * @return {?String} + * @see MatrixClient#event:"sync" */ - - getSyncState() { return this.syncState; } + /** * Returns the additional data object associated with * the current sync state, or null if there is no * such data. * Sync errors, if available, are put in the 'error' key of * this object. - * @return {?Object} */ + getSyncStateData() { + return this.syncStateData ?? null; + } + // Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts - getSyncStateData() { - return this.syncStateData; + createRoom(roomId) { + // XXX cargoculted from sync.ts + const { + timelineSupport + } = this.client; + const room = new _room.Room(roomId, this.client, this.client.getUserId(), { + lazyLoadMembers: this.opts.lazyLoadMembers, + pendingEventOrdering: this.opts.pendingEventOrdering, + timelineSupport + }); + this.client.reEmitter.reEmit(room, [_room.RoomEvent.Name, _room.RoomEvent.Redaction, _room.RoomEvent.RedactionCancelled, _room.RoomEvent.Receipt, _room.RoomEvent.Tags, _room.RoomEvent.LocalEchoUpdated, _room.RoomEvent.AccountData, _room.RoomEvent.MyMembership, _room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset]); + this.registerStateListeners(room); + return room; + } + registerStateListeners(room) { + // XXX cargoculted from sync.ts + // we need to also re-emit room state and room member events, so hook it up + // to the client now. We need to add a listener for RoomState.members in + // order to hook them correctly. + this.client.reEmitter.reEmit(room.currentState, [_roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update]); + room.currentState.on(_roomState.RoomStateEvent.NewMember, (event, state, member) => { + member.user = this.client.getUser(member.userId) ?? undefined; + this.client.reEmitter.reEmit(member, [_roomMember.RoomMemberEvent.Name, _roomMember.RoomMemberEvent.Typing, _roomMember.RoomMemberEvent.PowerLevel, _roomMember.RoomMemberEvent.Membership]); + }); } + /* + private deregisterStateListeners(room: Room): void { // XXX cargoculted from sync.ts + // could do with a better way of achieving this. + room.currentState.removeAllListeners(RoomStateEvent.Events); + room.currentState.removeAllListeners(RoomStateEvent.Members); + room.currentState.removeAllListeners(RoomStateEvent.NewMember); + } */ + shouldAbortSync(error) { if (error.errcode === "M_UNKNOWN_TOKEN") { // The logout already happened, we just need to stop. _logger.logger.warn("Token no longer valid - assuming logout"); - this.stop(); this.updateSyncState(_sync.SyncState.Error, { error }); return true; } - return false; } - async processRoomData(client, room, roomData) { roomData = ensureNameEvent(client, room.roomId, roomData); - const stateEvents = mapEvents(this.client, room.roomId, roomData.required_state); // Prevent events from being decrypted ahead of time + const stateEvents = mapEvents(this.client, room.roomId, roomData.required_state); + // Prevent events from being decrypted ahead of time // this helps large account to speed up faster // room::decryptCriticalEvent is in charge of decrypting all the events // required for a client to function properly - let timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false); const ephemeralEvents = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral); + // TODO: handle threaded / beacon events if (roomData.initial) { @@ -434,7 +444,8 @@ const knownEvents = new Set(); room.getLiveTimeline().getEvents().forEach(e => { knownEvents.add(e.getId()); - }); // all unknown events BEFORE a known event must be scrollback e.g: + }); + // all unknown events BEFORE a known event must be scrollback e.g: // D E <-- what we know // A B C D E F <-- what we just received // means: @@ -442,14 +453,11 @@ // D E <-- dupes // F <-- new event // We bucket events based on if we have seen a known event yet. - const oldEvents = []; const newEvents = []; let seenKnownEvent = false; - for (let i = timelineEvents.length - 1; i >= 0; i--) { const recvEvent = timelineEvents[i]; - if (knownEvents.has(recvEvent.getId())) { seenKnownEvent = true; continue; // don't include this event, it's a dupe @@ -463,21 +471,17 @@ newEvents.unshift(recvEvent); } } - timelineEvents = newEvents; - if (oldEvents.length > 0) { // old events are scrollback, insert them now room.addEventsToTimeline(oldEvents, true, room.getLiveTimeline(), roomData.prev_batch); } } - - const encrypted = this.client.isRoomEncrypted(room.roomId); // we do this first so it's correct when any of the events fire - + const encrypted = this.client.isRoomEncrypted(room.roomId); + // we do this first so it's correct when any of the events fire if (roomData.notification_count != null) { room.setUnreadNotificationCount(_room.NotificationCountType.Total, roomData.notification_count); } - if (roomData.highlight_count != null) { // We track unread notifications ourselves in encrypted rooms, so don't // bother setting it here. We trust our calculations better than the @@ -487,37 +491,32 @@ room.setUnreadNotificationCount(_room.NotificationCountType.Highlight, roomData.highlight_count); } } - if (Number.isInteger(roomData.invited_count)) { room.currentState.setInvitedMemberCount(roomData.invited_count); } - if (Number.isInteger(roomData.joined_count)) { room.currentState.setJoinedMemberCount(roomData.joined_count); } - if (roomData.invite_state) { const inviteStateEvents = mapEvents(this.client, room.roomId, roomData.invite_state); - this.processRoomEvents(room, inviteStateEvents); - + this.injectRoomEvents(room, inviteStateEvents); if (roomData.initial) { room.recalculate(); this.client.store.storeRoom(room); this.client.emit(_client.ClientEvent.Room, room); } - inviteStateEvents.forEach(e => { this.client.emit(_client.ClientEvent.Event, e); }); room.updateMyMembership("invite"); return; } - if (roomData.initial) { // set the back-pagination token. Do this *before* adding any // events so that clients can start back-paginating. - room.getLiveTimeline().setPaginationToken(roomData.prev_batch, _eventTimeline.EventTimeline.BACKWARDS); + room.getLiveTimeline().setPaginationToken(roomData.prev_batch ?? null, _eventTimeline.EventTimeline.BACKWARDS); } + /* TODO else if (roomData.limited) { let limited = true; @@ -555,69 +554,69 @@ if (limited) { room.resetLiveTimeline( roomData.prev_batch, - null, // TODO this.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken, + null, // TODO this.syncOpts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken, ); // We have to assume any gap in any timeline is // reason to stop incrementally tracking notifications and // reset the timeline. this.client.resetNotifTimelineSet(); + this.registerStateListeners(room); } } */ + this.injectRoomEvents(room, stateEvents, timelineEvents, roomData.num_live); - this.processRoomEvents(room, stateEvents, timelineEvents, false); // we deliberately don't add ephemeral events to the timeline + // we deliberately don't add ephemeral events to the timeline + room.addEphemeralEvents(ephemeralEvents); - room.addEphemeralEvents(ephemeralEvents); // local fields must be set before any async calls because call site assumes + // local fields must be set before any async calls because call site assumes // synchronous execution prior to emitting SlidingSyncState.Complete - room.updateMyMembership("join"); room.recalculate(); - if (roomData.initial) { client.store.storeRoom(room); client.emit(_client.ClientEvent.Room, room); - } // check if any timeline events should bing and add them to the notifEvents array: - // we'll purge this once we've fully processed the sync response - + } + // check if any timeline events should bing and add them to the notifEvents array: + // we'll purge this once we've fully processed the sync response this.addNotifications(timelineEvents); - const processRoomEvent = async e => { client.emit(_client.ClientEvent.Event, e); - - if (e.isState() && e.getType() == _matrix.EventType.RoomEncryption && this.opts.crypto) { - await this.opts.crypto.onCryptoEvent(e); + if (e.isState() && e.getType() == _event.EventType.RoomEncryption && this.syncOpts.cryptoCallbacks) { + await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e); } }; - await utils.promiseMapSeries(stateEvents, processRoomEvent); await utils.promiseMapSeries(timelineEvents, processRoomEvent); ephemeralEvents.forEach(function (e) { client.emit(_client.ClientEvent.Event, e); - }); // Decrypt only the last message in all rooms to make sure we can generate a preview + }); + + // Decrypt only the last message in all rooms to make sure we can generate a preview // And decrypt all events after the recorded read receipt to ensure an accurate // notification count - room.decryptCriticalEvents(); } + /** - * @param {Room} room - * @param {MatrixEvent[]} stateEventList A list of state events. This is the state + * Injects events into a room's model. + * @param stateEventList - A list of state events. This is the state * at the *START* of the timeline list if it is supplied. - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index - * @param {boolean} fromCache whether the sync response came from cache + * @param timelineEventList - A list of timeline events. Lower index * is earlier in time. Higher index is later. + * @param numLive - the number of events in timelineEventList which just happened, + * supplied from the server. */ - - - processRoomEvents(room, stateEventList, timelineEventList, fromCache = false) { + injectRoomEvents(room, stateEventList, timelineEventList, numLive) { timelineEventList = timelineEventList || []; - stateEventList = stateEventList || []; // If there are no events in the timeline yet, initialise it with - // the given state events + stateEventList = stateEventList || []; + numLive = numLive || 0; + // If there are no events in the timeline yet, initialise it with + // the given state events const liveTimeline = room.getLiveTimeline(); const timelineWasEmpty = liveTimeline.getEvents().length == 0; - if (timelineWasEmpty) { // Passing these events into initialiseState will freeze them, so we need // to compute and cache the push actions for them now, otherwise sync dies @@ -630,9 +629,10 @@ for (const ev of stateEventList) { this.client.getPushActionsForEvent(ev); } - liveTimeline.initialiseState(stateEventList); - } // If the timeline wasn't empty, we process the state events here: they're + } + + // If the timeline wasn't empty, we process the state events here: they're // defined as updates to the state before the start of the timeline, so this // starts to roll the state forward. // XXX: That's what we *should* do, but this can happen if we were previously @@ -642,43 +642,57 @@ // very wrong because there could be events in the timeline that diverge the // state, in which case this is going to leave things out of sync. However, // for now I think it;s best to behave the same as the code has done previously. - - if (!timelineWasEmpty) { // XXX: As above, don't do this... //room.addLiveEvents(stateEventList || []); // Do this instead... room.oldState.setStateEvents(stateEventList); room.currentState.setStateEvents(stateEventList); - } // execute the timeline events. This will continue to diverge the current state + } + + // the timeline is broken into 'live' events which just happened and normal timeline events + // which are still to be appended to the end of the live timeline but happened a while ago. + // The live events are marked as fromCache=false to ensure that downstream components know + // this is a live event, not historical (from a remote server cache). + + let liveTimelineEvents = []; + if (numLive > 0) { + // last numLive events are live + liveTimelineEvents = timelineEventList.slice(-1 * numLive); + // everything else is not live + timelineEventList = timelineEventList.slice(0, -1 * liveTimelineEvents.length); + } + + // execute the timeline events. This will continue to diverge the current state // if the timeline has any state events in it. // This also needs to be done before running push rules on the events as they need // to be decorated with sender etc. - - room.addLiveEvents(timelineEventList, { - fromCache: fromCache + fromCache: true }); - room.recalculate(); // resolve invites now we have set the latest state + if (liveTimelineEvents.length > 0) { + room.addLiveEvents(liveTimelineEvents, { + fromCache: false + }); + } + room.recalculate(); + // resolve invites now we have set the latest state this.resolveInvites(room); } - resolveInvites(room) { if (!room || !this.opts.resolveInvitesToProfiles) { return; } - - const client = this.client; // For each invited room member we want to give them a displayname/avatar url + const client = this.client; + // For each invited room member we want to give them a displayname/avatar url // if they have one (the m.room.member invites don't contain this). - room.getMembersWithMembership("invite").forEach(function (member) { - if (member._requestedProfileInfo) return; - member._requestedProfileInfo = true; // try to get a cached copy first. - + if (member.requestedProfileInfo) return; + member.requestedProfileInfo = true; + // try to get a cached copy first. const user = client.getUser(member.userId); let promise; - if (user) { promise = Promise.resolve({ avatar_url: user.avatarUrl, @@ -687,109 +701,96 @@ } else { promise = client.getProfileInfo(member.userId); } - promise.then(function (info) { // slightly naughty by doctoring the invite event but this means all // the code paths remain the same between invite/join display name stuff // which is a worthy trade-off for some minor pollution. const inviteEvent = member.events.member; - if (inviteEvent.getContent().membership !== "invite") { // between resolving and now they have since joined, so don't clobber return; } - inviteEvent.getContent().avatar_url = info.avatar_url; - inviteEvent.getContent().displayname = info.displayname; // fire listeners - + inviteEvent.getContent().displayname = info.displayname; + // fire listeners member.setMembershipEvent(inviteEvent, room.currentState); - }, function (_err) {// OH WELL. + }, function (_err) { + // OH WELL. }); }); } - retryImmediately() { return true; } + /** * Main entry point. Blocks until stop() is called. */ - - async sync() { - _logger.logger.debug("Sliding sync init loop"); // 1) We need to get push rules so we can check if events should bing as we get - // them from /sync. - + _logger.logger.debug("Sliding sync init loop"); + // 1) We need to get push rules so we can check if events should bing as we get + // them from /sync. while (!this.client.isGuest()) { try { _logger.logger.debug("Getting push rules..."); - const result = await this.client.getPushRules(); - _logger.logger.debug("Got push rules"); - this.client.pushRules = result; break; } catch (err) { _logger.logger.error("Getting push rules failed", err); - if (this.shouldAbortSync(err)) { return; } } - } // start syncing - + } + // start syncing await this.slidingSync.start(); } + /** * Stops the sync object from syncing. */ - - stop() { _logger.logger.debug("SyncApi.stop"); - this.slidingSync.stop(); } + /** * Sets the sync state and emits an event to say so - * @param {String} newState The new state string - * @param {Object} data Object of additional data to emit in the event + * @param newState - The new state string + * @param data - Object of additional data to emit in the event */ - - updateSyncState(newState, data) { const old = this.syncState; this.syncState = newState; this.syncStateData = data; this.client.emit(_client.ClientEvent.Sync, this.syncState, old, data); } + /** * Takes a list of timelineEvents and adds and adds to notifEvents * as appropriate. * This must be called after the room the events belong to has been stored. * - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * @param timelineEventList - A list of timeline events. Lower index * is earlier in time. Higher index is later. */ - - addNotifications(timelineEventList) { // gather our notifications into this.notifEvents if (!this.client.getNotifTimelineSet()) { return; } - for (const timelineEvent of timelineEventList) { const pushActions = this.client.getPushActionsForEvent(timelineEvent); - if (pushActions && pushActions.notify && pushActions.tweaks && pushActions.tweaks.highlight) { this.notifEvents.push(timelineEvent); } } } + /** * Purge any events in the notifEvents array. Used after a /sync has been complete. * This should not be called at a per-room scope (e.g in onRoomData) because otherwise the ordering @@ -797,22 +798,17 @@ * response. If we purge at a per-room scope then we could process room B before room A leading to * room B appearing earlier in the notifications timeline, even though it has the higher origin_server_ts. */ - - purgeNotifications() { this.notifEvents.sort(function (a, b) { return a.getTs() - b.getTs(); }); this.notifEvents.forEach(event => { - this.client.getNotifTimelineSet().addLiveEvent(event); + this.client.getNotifTimelineSet()?.addLiveEvent(event); }); this.notifEvents = []; } - } - exports.SlidingSyncSdk = SlidingSyncSdk; - function ensureNameEvent(client, roomId, roomData) { // make sure m.room.name is in required_state if there is a name, replacing anything previously // there if need be. This ensures clients transparently 'calculate' the right room name. Native @@ -820,20 +816,18 @@ if (!roomData.name) { return roomData; } - for (const stateEvent of roomData.required_state) { - if (stateEvent.type === _matrix.EventType.RoomName && stateEvent.state_key === "") { + if (stateEvent.type === _event.EventType.RoomName && stateEvent.state_key === "") { stateEvent.content = { name: roomData.name }; return roomData; } } - roomData.required_state.push({ event_id: "$fake-sliding-sync-name-event-" + roomId, state_key: "", - type: _matrix.EventType.RoomName, + type: _event.EventType.RoomName, content: { name: roomData.name }, @@ -841,16 +835,27 @@ origin_server_ts: new Date().getTime() }); return roomData; -} // Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts, +} +// Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts, // just outside the class. - - function mapEvents(client, roomId, events, decrypt = true) { const mapper = client.getEventMapper({ decrypt }); return events.map(function (e) { - e["room_id"] = roomId; + e.room_id = roomId; return mapper(e); }); +} +function processEphemeralEvents(client, roomId, ephEvents) { + const ephemeralEvents = mapEvents(client, roomId, ephEvents); + const room = client.getRoom(roomId); + if (!room) { + _logger.logger.warn("got ephemeral events for room but room doesn't exist on client:", roomId); + return; + } + room.addEphemeralEvents(ephemeralEvents); + ephemeralEvents.forEach(e => { + client.emit(_client.ClientEvent.Event, e); + }); } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,27 +4,20 @@ value: true }); exports.IndexedDBStore = void 0; - var _memory = require("./memory"); - var _indexeddbLocalBackend = require("./indexeddb-local-backend"); - var _indexeddbRemoteBackend = require("./indexeddb-remote-backend"); - var _user = require("../models/user"); - var _event = require("../models/event"); - var _logger = require("../logger"); - var _typedEventEmitter = require("../models/typed-event-emitter"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * This is an internal module. See {@link IndexedDBStore} for the public class. - * @module store/indexeddb */ + // If this value is too small we'll be writing very often which will cause // noticeable stop-the-world pauses. If this value is too big we'll be writing // so infrequently that the /sync size gets bigger on reload. Writing more @@ -38,16 +31,21 @@ } /** + * The backend instance. + * Call through to this API if you need to perform specific indexeddb actions like deleting the database. + */ + + /** * Construct a new Indexed Database store, which extends MemoryStore. * * This store functions like a MemoryStore except it periodically persists * the contents of the store to an IndexedDB backend. * * All data is still kept in-memory but can be loaded from disk by calling - * startup(). This can make startup times quicker as a complete + * `startup()`. This can make startup times quicker as a complete * sync from the server is not required. This does not reduce memory usage as all - * the data is eagerly fetched when startup() is called. - *

+   * the data is eagerly fetched when `startup()` is called.
+   * ```
    * let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
    * let store = new IndexedDBStore(opts);
    * await store.startup(); // load from indexed db
@@ -60,187 +58,140 @@
    *         console.log("Started up, now with go faster stripes!");
    *     }
    * });
-   * 
+ * ``` * - * @constructor - * @extends MemoryStore - * @param {Object} opts Options object. - * @param {Object} opts.indexedDB The Indexed DB interface e.g. - * window.indexedDB - * @param {string=} opts.dbName Optional database name. The same name must be used - * to open the same database. - * @param {string=} opts.workerScript Optional URL to a script to invoke a web - * worker with to run IndexedDB queries on the web worker. The IndexedDbStoreWorker - * class is provided for this purpose and requires the application to provide a - * trivial wrapper script around it. - * @param {Object=} opts.workerApi The webWorker API object. If omitted, the global Worker - * object will be used if it exists. - * @prop {IndexedDBStoreBackend} backend The backend instance. Call through to - * this API if you need to perform specific indexeddb actions like deleting the - * database. + * @param opts - Options object. */ constructor(opts) { super(opts); - _defineProperty(this, "backend", void 0); - _defineProperty(this, "startedUp", false); - _defineProperty(this, "syncTs", 0); - _defineProperty(this, "userModifiedMap", {}); - _defineProperty(this, "emitter", new _typedEventEmitter.TypedEventEmitter()); - _defineProperty(this, "on", this.emitter.on.bind(this.emitter)); - _defineProperty(this, "getSavedSync", this.degradable(() => { return this.backend.getSavedSync(); }, "getSavedSync")); - _defineProperty(this, "isNewlyCreated", this.degradable(() => { return this.backend.isNewlyCreated(); }, "isNewlyCreated")); - _defineProperty(this, "getSavedSyncToken", this.degradable(() => { return this.backend.getNextBatchToken(); }, "getSavedSyncToken")); - _defineProperty(this, "deleteAllData", this.degradable(() => { super.deleteAllData(); return this.backend.clearDatabase().then(() => { _logger.logger.log("Deleted indexeddb data."); }, err => { _logger.logger.error(`Failed to delete indexeddb data: ${err}`); - throw err; }); })); - _defineProperty(this, "reallySave", this.degradable(() => { this.syncTs = Date.now(); // set now to guard against multi-writes + // work out changed users (this doesn't handle deletions but you // can't 'delete' users as they are just presence events). - const userTuples = []; - for (const u of this.getUsers()) { if (this.userModifiedMap[u.userId] === u.getLastModifiedTime()) continue; if (!u.events.presence) continue; - userTuples.push([u.userId, u.events.presence.event]); // note that we've saved this version of the user + userTuples.push([u.userId, u.events.presence.event]); + // note that we've saved this version of the user this.userModifiedMap[u.userId] = u.getLastModifiedTime(); } - return this.backend.syncToDatabase(userTuples); })); - _defineProperty(this, "setSyncData", this.degradable(syncData => { return this.backend.setSyncData(syncData); }, "setSyncData")); - _defineProperty(this, "getOutOfBandMembers", this.degradable(roomId => { return this.backend.getOutOfBandMembers(roomId); }, "getOutOfBandMembers")); - _defineProperty(this, "setOutOfBandMembers", this.degradable((roomId, membershipEvents) => { super.setOutOfBandMembers(roomId, membershipEvents); return this.backend.setOutOfBandMembers(roomId, membershipEvents); }, "setOutOfBandMembers")); - _defineProperty(this, "clearOutOfBandMembers", this.degradable(roomId => { super.clearOutOfBandMembers(roomId); return this.backend.clearOutOfBandMembers(roomId); }, "clearOutOfBandMembers")); - _defineProperty(this, "getClientOptions", this.degradable(() => { return this.backend.getClientOptions(); }, "getClientOptions")); - _defineProperty(this, "storeClientOptions", this.degradable(options => { super.storeClientOptions(options); return this.backend.storeClientOptions(options); }, "storeClientOptions")); - if (!opts.indexedDB) { - throw new Error('Missing required option: indexedDB'); + throw new Error("Missing required option: indexedDB"); } - if (opts.workerFactory) { this.backend = new _indexeddbRemoteBackend.RemoteIndexedDBStoreBackend(opts.workerFactory, opts.dbName); } else { this.backend = new _indexeddbLocalBackend.LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName); } } - /** - * @return {Promise} Resolved when loaded from indexed db. + * @returns Resolved when loaded from indexed db. */ startup() { if (this.startedUp) { _logger.logger.log(`IndexedDBStore.startup: already started`); - return Promise.resolve(); } - _logger.logger.log(`IndexedDBStore.startup: connecting to backend`); - return this.backend.connect().then(() => { _logger.logger.log(`IndexedDBStore.startup: loading presence events`); - return this.backend.getUserPresenceEvents(); }).then(userPresenceEvents => { _logger.logger.log(`IndexedDBStore.startup: processing presence events`); - userPresenceEvents.forEach(([userId, rawEvent]) => { const u = new _user.User(userId); - if (rawEvent) { u.setPresenceEvent(new _event.MatrixEvent(rawEvent)); } - this.userModifiedMap[u.userId] = u.getLastModifiedTime(); this.storeUser(u); }); }); } + /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ - /** * Whether this store would like to save its data * Note that obviously whether the store wants to save or * not could change between calling this function and calling * save(). * - * @return {boolean} True if calling save() will actually save + * @returns True if calling save() will actually save * (at the time this function is called). */ wantsSave() { const now = Date.now(); return now - this.syncTs > WRITE_DELAY_MS; } + /** * Possibly write data to the database. * - * @param {boolean} force True to force a save to happen - * @return {Promise} Promise resolves after the write completes + * @param force - True to force a save to happen + * @returns Promise resolves after the write completes * (or immediately if no write is performed) */ - - save(force = false) { if (force || this.wantsSave()) { return this.reallySave(); } - return Promise.resolve(); } - /** * All member functions of `IndexedDBStore` that access the backend use this wrapper to * watch for failures after initial store startup, including `QuotaExceededError` as @@ -249,54 +200,48 @@ * When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore` * in place so that the current operation and all future ones are in-memory only. * - * @param {Function} func The degradable work to do. - * @param {String} fallback The method name for fallback. - * @returns {Function} A wrapped member function. + * @param func - The degradable work to do. + * @param fallback - The method name for fallback. + * @returns A wrapped member function. */ degradable(func, fallback) { - const fallbackFn = super[fallback]; + const fallbackFn = fallback ? super[fallback] : null; return async (...args) => { try { return await func.call(this, ...args); } catch (e) { _logger.logger.error("IndexedDBStore failure, degrading to MemoryStore", e); - this.emitter.emit("degraded", e); - try { // We try to delete IndexedDB after degrading since this store is only a // cache (the app will still function correctly without the data). // It's possible that deleting repair IndexedDB for the next app load, // potentially by making a little more space available. _logger.logger.log("IndexedDBStore trying to delete degraded data"); - await this.backend.clearDatabase(); - _logger.logger.log("IndexedDBStore delete after degrading succeeded"); } catch (e) { _logger.logger.warn("IndexedDBStore delete after degrading failed", e); - } // Degrade the store from being an instance of `IndexedDBStore` to instead be + } + // Degrade the store from being an instance of `IndexedDBStore` to instead be // an instance of `MemoryStore` so that future API calls use the memory path // directly and skip IndexedDB entirely. This should be safe as // `IndexedDBStore` already extends from `MemoryStore`, so we are making the // store become its parent type in a way. The mutator methods of // `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are // not overridden at all). - - if (fallbackFn) { return fallbackFn.call(this, ...args); } } }; - } // XXX: ideally these would be stored in indexeddb as part of the room but, - // we don't store rooms as such and instead accumulate entire sync responses atm. - + } + // XXX: ideally these would be stored in indexeddb as part of the room but, + // we don't store rooms as such and instead accumulate entire sync responses atm. async getPendingEvents(roomId) { if (!this.localStorage) return super.getPendingEvents(roomId); const serialized = this.localStorage.getItem(pendingEventsKey(roomId)); - if (serialized) { try { return JSON.parse(serialized); @@ -304,41 +249,32 @@ _logger.logger.error("Could not parse persisted pending events", e); } } - return []; } - async setPendingEvents(roomId, events) { if (!this.localStorage) return super.setPendingEvents(roomId, events); - if (events.length > 0) { this.localStorage.setItem(pendingEventsKey(roomId), JSON.stringify(events)); } else { this.localStorage.removeItem(pendingEventsKey(roomId)); } } - saveToDeviceBatches(batches) { return this.backend.saveToDeviceBatches(batches); } - getOldestToDeviceBatch() { return this.backend.getOldestToDeviceBatch(); } - removeToDeviceBatch(id) { return this.backend.removeToDeviceBatch(id); } - } + /** - * @param {string} roomId ID of the current room - * @returns {string} Storage key to retrieve pending events + * @param roomId - ID of the current room + * @returns Storage key to retrieve pending events */ - - exports.IndexedDBStore = IndexedDBStore; - function pendingEventsKey(roomId) { return `mx_pending_events_${roomId}`; } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-local-backend.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-local-backend.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-local-backend.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-local-backend.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,82 +4,70 @@ value: true }); exports.LocalIndexedDBStoreBackend = void 0; - var _syncAccumulator = require("../sync-accumulator"); - var utils = _interopRequireWildcard(require("../utils")); - var IndexedDBHelpers = _interopRequireWildcard(require("../indexeddb-helpers")); - var _logger = require("../logger"); - function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -const VERSION = 4; - -function createDatabase(db) { +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +const DB_MIGRATIONS = [db => { // Make user store, clobber based on user ID. (userId property of User objects) db.createObjectStore("users", { keyPath: ["userId"] - }); // Make account data store, clobber based on event type. - // (event.type property of MatrixEvent objects) + }); + // Make account data store, clobber based on event type. + // (event.type property of MatrixEvent objects) db.createObjectStore("accountData", { keyPath: ["type"] - }); // Make /sync store (sync tokens, room data, etc), always clobber (const key). + }); + // Make /sync store (sync tokens, room data, etc), always clobber (const key). db.createObjectStore("sync", { keyPath: ["clobber"] }); -} - -function upgradeSchemaV2(db) { +}, db => { const oobMembersStore = db.createObjectStore("oob_membership_events", { keyPath: ["room_id", "state_key"] }); oobMembersStore.createIndex("room", "room_id"); -} - -function upgradeSchemaV3(db) { +}, db => { db.createObjectStore("client_options", { keyPath: ["clobber"] }); -} - -function upgradeSchemaV4(db) { +}, db => { db.createObjectStore("to_device_queue", { autoIncrement: true }); } +// Expand as needed. +]; + +const VERSION = DB_MIGRATIONS.length; + /** * Helper method to collect results from a Cursor and promiseify it. - * @param {ObjectStore|Index} store The store to perform openCursor on. - * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor. - * @param {Function} resultMapper A function which is repeatedly called with a + * @param store - The store to perform openCursor on. + * @param keyRange - Optional key range to apply on the cursor. + * @param resultMapper - A function which is repeatedly called with a * Cursor. * Return the data you want to keep. - * @return {Promise} Resolves to an array of whatever you returned from + * @returns Promise which resolves to an array of whatever you returned from * resultMapper. */ - - function selectQuery(store, keyRange, resultMapper) { const query = store.openCursor(keyRange); return new Promise((resolve, reject) => { const results = []; - query.onerror = () => { reject(new Error("Query failed: " + query.error)); - }; // collect results - - + }; + // collect results query.onsuccess = () => { const cursor = query.result; - if (!cursor) { resolve(results); return; // end of results @@ -90,161 +78,117 @@ }; }); } - function txnAsPromise(txn) { return new Promise((resolve, reject) => { txn.oncomplete = function (event) { resolve(event); }; - txn.onerror = function () { reject(txn.error); }; }); } - function reqAsEventPromise(req) { return new Promise((resolve, reject) => { req.onsuccess = function (event) { resolve(event); }; - req.onerror = function () { reject(req.error); }; }); } - function reqAsPromise(req) { return new Promise((resolve, reject) => { req.onsuccess = () => resolve(req); - req.onerror = err => reject(err); }); } - function reqAsCursorPromise(req) { return reqAsEventPromise(req).then(event => req.result); } - class LocalIndexedDBStoreBackend { static exists(indexedDB, dbName) { dbName = "matrix-js-sdk:" + (dbName || "default"); return IndexedDBHelpers.exists(indexedDB, dbName); } - /** * Does the actual reading from and writing to the indexeddb * * Construct a new Indexed Database store backend. This requires a call to - * connect() before this store can be used. - * @constructor - * @param {Object} indexedDB The Indexed DB interface e.g - * window.indexedDB - * @param {string=} dbName Optional database name. The same name must be used + * `connect()` before this store can be used. + * @param indexedDB - The Indexed DB interface e.g + * `window.indexedDB` + * @param dbName - Optional database name. The same name must be used * to open the same database. */ - constructor(indexedDB, dbName) { + constructor(indexedDB, dbName = "default") { this.indexedDB = indexedDB; - _defineProperty(this, "dbName", void 0); - _defineProperty(this, "syncAccumulator", void 0); - - _defineProperty(this, "db", null); - + _defineProperty(this, "db", void 0); _defineProperty(this, "disconnected", true); - _defineProperty(this, "_isNewlyCreated", false); - _defineProperty(this, "isPersisting", false); - _defineProperty(this, "pendingUserPresenceData", []); - - this.dbName = "matrix-js-sdk:" + (dbName || "default"); + this.dbName = "matrix-js-sdk:" + dbName; this.syncAccumulator = new _syncAccumulator.SyncAccumulator(); } + /** * Attempt to connect to the database. This can fail if the user does not * grant permission. - * @return {Promise} Resolves if successfully connected. + * @returns Promise which resolves if successfully connected. */ - - connect() { if (!this.disconnected) { _logger.logger.log(`LocalIndexedDBStoreBackend.connect: already connected or connecting`); - return Promise.resolve(); } - this.disconnected = false; - _logger.logger.log(`LocalIndexedDBStoreBackend.connect: connecting...`); - const req = this.indexedDB.open(this.dbName, VERSION); - req.onupgradeneeded = ev => { const db = req.result; const oldVersion = ev.oldVersion; - _logger.logger.log(`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`); - if (oldVersion < 1) { - // The database did not previously exist. + // The database did not previously exist this._isNewlyCreated = true; - createDatabase(db); - } - - if (oldVersion < 2) { - upgradeSchemaV2(db); } - - if (oldVersion < 3) { - upgradeSchemaV3(db); - } - - if (oldVersion < 4) { - upgradeSchemaV4(db); - } // Expand as needed. - + DB_MIGRATIONS.forEach((migration, index) => { + if (oldVersion <= index) migration(db); + }); }; - req.onblocked = () => { _logger.logger.log(`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`); }; - _logger.logger.log(`LocalIndexedDBStoreBackend.connect: awaiting connection...`); - - return reqAsEventPromise(req).then(() => { + return reqAsEventPromise(req).then(async () => { _logger.logger.log(`LocalIndexedDBStoreBackend.connect: connected`); + this.db = req.result; - this.db = req.result; // add a poorly-named listener for when deleteDatabase is called + // add a poorly-named listener for when deleteDatabase is called // so we can close our db connections. - this.db.onversionchange = () => { - this.db.close(); + this.db?.close(); }; - - return this.init(); + await this.init(); }); } - /** @return {boolean} whether or not the database was newly created in this session. */ - + /** @returns whether or not the database was newly created in this session. */ isNewlyCreated() { return Promise.resolve(this._isNewlyCreated); } + /** * Having connected, load initial data from the database and prepare for use - * @return {Promise} Resolves on success + * @returns Promise which resolves on success */ - - init() { return Promise.all([this.loadAccountData(), this.loadSyncData()]).then(([accountData, syncData]) => { _logger.logger.log(`LocalIndexedDBStoreBackend: loaded initial data`); - this.syncAccumulator.accumulate({ next_batch: syncData.nextBatch, rooms: syncData.roomsData, @@ -254,15 +198,13 @@ }, true); }); } + /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {Promise} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ - - getOutOfBandMembers(roomId) { return new Promise((resolve, reject) => { const tx = this.db.transaction(["oob_membership_events"], "readonly"); @@ -270,68 +212,57 @@ const roomIndex = store.index("room"); const range = IDBKeyRange.only(roomId); const request = roomIndex.openCursor(range); - const membershipEvents = []; // did we encounter the oob_written marker object + const membershipEvents = []; + // did we encounter the oob_written marker object // amongst the results? That means OOB member // loading already happened for this room // but there were no members to persist as they // were all known already - let oobWritten = false; - request.onsuccess = () => { const cursor = request.result; - if (!cursor) { // Unknown room if (!membershipEvents.length && !oobWritten) { return resolve(null); } - return resolve(membershipEvents); } - const record = cursor.value; - if (record.oob_written) { oobWritten = true; } else { membershipEvents.push(record); } - cursor.continue(); }; - request.onerror = err => { reject(err); }; }).then(events => { _logger.logger.log(`LL: got ${events?.length} membershipEvents from storage for room ${roomId} ...`); - return events; }); } + /** * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store + * @param membershipEvents - the membership events to store */ - - async setOutOfBandMembers(roomId, membershipEvents) { _logger.logger.log(`LL: backend about to store ${membershipEvents.length}` + ` members for ${roomId}`); - const tx = this.db.transaction(["oob_membership_events"], "readwrite"); const store = tx.objectStore("oob_membership_events"); membershipEvents.forEach(e => { store.put(e); - }); // aside from all the events, we also write a marker object to the store + }); + // aside from all the events, we also write a marker object to the store // to mark the fact that OOB members have been written for this room. // It's possible that 0 members need to be written as all where previously know // but we still need to know whether to return null or [] from getOutOfBandMembers // where null means out of band members haven't been stored yet for this room - const markerObject = { room_id: roomId, oob_written: true, @@ -339,10 +270,8 @@ }; store.put(markerObject); await txnAsPromise(tx); - _logger.logger.log(`LL: backend done storing for ${roomId}!`); } - async clearOutOfBandMembers(roomId) { // the approach to delete all members for a room // is to get the min and max state key from the index @@ -354,65 +283,54 @@ const store = readTx.objectStore("oob_membership_events"); const roomIndex = store.index("room"); const roomRange = IDBKeyRange.only(roomId); - const minStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "next")).then(cursor => cursor && cursor.primaryKey[1]); - const maxStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "prev")).then(cursor => cursor && cursor.primaryKey[1]); + const minStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "next")).then(cursor => (cursor?.primaryKey)[1]); + const maxStateKeyProm = reqAsCursorPromise(roomIndex.openKeyCursor(roomRange, "prev")).then(cursor => (cursor?.primaryKey)[1]); const [minStateKey, maxStateKey] = await Promise.all([minStateKeyProm, maxStateKeyProm]); const writeTx = this.db.transaction(["oob_membership_events"], "readwrite"); const writeStore = writeTx.objectStore("oob_membership_events"); const membersKeyRange = IDBKeyRange.bound([roomId, minStateKey], [roomId, maxStateKey]); - _logger.logger.log(`LL: Deleting all users + marker in storage for room ${roomId}, with key range:`, [roomId, minStateKey], [roomId, maxStateKey]); - await reqAsPromise(writeStore.delete(membersKeyRange)); } + /** * Clear the entire database. This should be used when logging out of a client * to prevent mixing data between accounts. - * @return {Promise} Resolved when the database is cleared. + * @returns Resolved when the database is cleared. */ - - clearDatabase() { return new Promise(resolve => { _logger.logger.log(`Removing indexeddb instance: ${this.dbName}`); - const req = this.indexedDB.deleteDatabase(this.dbName); - req.onblocked = () => { _logger.logger.log(`can't yet delete indexeddb ${this.dbName} because it is open elsewhere`); }; - req.onerror = () => { // in firefox, with indexedDB disabled, this fails with a // DOMError. We treat this as non-fatal, so that we can still // use the app. _logger.logger.warn(`unable to delete js-sdk store indexeddb: ${req.error}`); - resolve(); }; - req.onsuccess = () => { _logger.logger.log(`Removed indexeddb instance: ${this.dbName}`); - resolve(); }; }); } + /** - * @param {boolean=} copy If false, the data returned is from internal + * @param copy - If false, the data returned is from internal * buffers and must not be mutated. Otherwise, a copy is made before * returning such that the data can be safely mutated. Default: true. * - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ - - getSavedSync(copy = true) { const data = this.syncAccumulator.getJSON(); if (!data.nextBatch) return Promise.resolve(null); - if (copy) { // We must deep copy the stored data so that the /sync processing code doesn't // corrupt the internal state of the sync accumulator (it adds non-clonable keys) @@ -421,28 +339,23 @@ return Promise.resolve(data); } } - getNextBatchToken() { return Promise.resolve(this.syncAccumulator.getNextBatchToken()); } - setSyncData(syncData) { return Promise.resolve().then(() => { this.syncAccumulator.accumulate(syncData); }); } - async syncToDatabase(userTuples) { if (this.isPersisting) { _logger.logger.warn("Skipping syncToDatabase() as persist already in flight"); - this.pendingUserPresenceData.push(...userTuples); return; } else { userTuples.unshift(...this.pendingUserPresenceData); this.isPersisting = true; } - try { const syncData = this.syncAccumulator.getJSON(true); await Promise.all([this.persistUserPresenceEvents(userTuples), this.persistAccountData(syncData.accountData), this.persistSyncData(syncData.nextBatch, syncData.roomsData)]); @@ -450,17 +363,15 @@ this.isPersisting = false; } } + /** * Persist rooms /sync data along with the next batch token. - * @param {string} nextBatch The next_batch /sync value. - * @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator - * @return {Promise} Resolves if the data was persisted. + * @param nextBatch - The next_batch /sync value. + * @param roomsData - The 'rooms' /sync data from a SyncAccumulator + * @returns Promise which resolves if the data was persisted. */ - - persistSyncData(nextBatch, roomsData) { _logger.logger.log("Persisting sync data up to", nextBatch); - return utils.promiseTry(() => { const txn = this.db.transaction(["sync"], "readwrite"); const store = txn.objectStore("sync"); @@ -470,47 +381,42 @@ nextBatch, roomsData }); // put == UPSERT - return txnAsPromise(txn).then(() => { _logger.logger.log("Persisted sync data up to", nextBatch); }); }); } + /** * Persist a list of account data events. Events with the same 'type' will * be replaced. - * @param {Object[]} accountData An array of raw user-scoped account data events - * @return {Promise} Resolves if the events were persisted. + * @param accountData - An array of raw user-scoped account data events + * @returns Promise which resolves if the events were persisted. */ - - persistAccountData(accountData) { return utils.promiseTry(() => { const txn = this.db.transaction(["accountData"], "readwrite"); const store = txn.objectStore("accountData"); - - for (let i = 0; i < accountData.length; i++) { - store.put(accountData[i]); // put == UPSERT + for (const event of accountData) { + store.put(event); // put == UPSERT } return txnAsPromise(txn).then(); }); } + /** * Persist a list of [user id, presence event] they are for. * Users with the same 'userId' will be replaced. * Presence events should be the event in its raw form (not the Event * object) - * @param {Object[]} tuples An array of [userid, event] tuples - * @return {Promise} Resolves if the users were persisted. + * @param tuples - An array of [userid, event] tuples + * @returns Promise which resolves if the users were persisted. */ - - persistUserPresenceEvents(tuples) { return utils.promiseTry(() => { const txn = this.db.transaction(["users"], "readwrite"); const store = txn.objectStore("users"); - for (const tuple of tuples) { store.put({ userId: tuple[0], @@ -521,14 +427,13 @@ return txnAsPromise(txn).then(); }); } + /** * Load all user presence events from the database. This is not cached. * FIXME: It would probably be more sensible to store the events in the * sync. - * @return {Promise} A list of presence events in their raw form. + * @returns A list of presence events in their raw form. */ - - getUserPresenceEvents() { return utils.promiseTry(() => { const txn = this.db.transaction(["users"], "readonly"); @@ -538,15 +443,13 @@ }); }); } + /** * Load all the account data events from the database. This is not cached. - * @return {Promise} A list of raw global account events. + * @returns A list of raw global account events. */ - - loadAccountData() { _logger.logger.log(`LocalIndexedDBStoreBackend: loading account data...`); - return utils.promiseTry(() => { const txn = this.db.transaction(["accountData"], "readonly"); const store = txn.objectStore("accountData"); @@ -554,20 +457,17 @@ return cursor.value; }).then(result => { _logger.logger.log(`LocalIndexedDBStoreBackend: loaded account data`); - return result; }); }); } + /** * Load the sync data from the database. - * @return {Promise} An object with "roomsData" and "nextBatch" keys. + * @returns An object with "roomsData" and "nextBatch" keys. */ - - loadSyncData() { _logger.logger.log(`LocalIndexedDBStoreBackend: loading sync data...`); - return utils.promiseTry(() => { const txn = this.db.transaction(["sync"], "readonly"); const store = txn.objectStore("sync"); @@ -575,16 +475,13 @@ return cursor.value; }).then(results => { _logger.logger.log(`LocalIndexedDBStoreBackend: loaded sync data`); - if (results.length > 1) { _logger.logger.warn("loadSyncData: More than 1 sync row found."); } - return results.length > 0 ? results[0] : {}; }); }); } - getClientOptions() { return Promise.resolve().then(() => { const txn = this.db.transaction(["client_options"], "readonly"); @@ -594,7 +491,6 @@ }).then(results => results[0]); }); } - async storeClientOptions(options) { const txn = this.db.transaction(["client_options"], "readwrite"); const store = txn.objectStore("client_options"); @@ -603,21 +499,16 @@ // constant key so will always clobber options: options }); // put == UPSERT - await txnAsPromise(txn); } - async saveToDeviceBatches(batches) { const txn = this.db.transaction(["to_device_queue"], "readwrite"); const store = txn.objectStore("to_device_queue"); - for (const batch of batches) { store.add(batch); } - await txnAsPromise(txn); } - async getOldestToDeviceBatch() { const txn = this.db.transaction(["to_device_queue"], "readonly"); const store = txn.objectStore("to_device_queue"); @@ -631,14 +522,11 @@ batch: resultBatch.batch }; } - async removeToDeviceBatch(id) { const txn = this.db.transaction(["to_device_queue"], "readwrite"); const store = txn.objectStore("to_device_queue"); store.delete(id); await txnAsPromise(txn); } - } - exports.LocalIndexedDBStoreBackend = LocalIndexedDBStoreBackend; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-remote-backend.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-remote-backend.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-remote-backend.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-remote-backend.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,13 +4,11 @@ value: true }); exports.RemoteIndexedDBStoreBackend = void 0; - var _logger = require("../logger"); - var _utils = require("../utils"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } class RemoteIndexedDBStoreBackend { // The currently in-flight requests to the actual backend // seq: promise @@ -22,45 +20,32 @@ * worker. * * Construct a new Indexed Database store backend. This requires a call to - * connect() before this store can be used. - * @constructor - * @param {Function} workerFactory Factory which produces a Worker - * @param {string=} dbName Optional database name. The same name must be used + * `connect()` before this store can be used. + * @param workerFactory - Factory which produces a Worker + * @param dbName - Optional database name. The same name must be used * to open the same database. */ constructor(workerFactory, dbName) { this.workerFactory = workerFactory; this.dbName = dbName; - _defineProperty(this, "worker", void 0); - _defineProperty(this, "nextSeq", 0); - _defineProperty(this, "inFlight", {}); - - _defineProperty(this, "startPromise", null); - + _defineProperty(this, "startPromise", void 0); _defineProperty(this, "onWorkerMessage", ev => { const msg = ev.data; - - if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') { + if (msg.command == "cmd_success" || msg.command == "cmd_fail") { if (msg.seq === undefined) { _logger.logger.error("Got reply from worker with no seq"); - return; } - const def = this.inFlight[msg.seq]; - if (def === undefined) { _logger.logger.error("Got reply for unknown seq " + msg.seq); - return; } - delete this.inFlight[msg.seq]; - - if (msg.command == 'cmd_success') { + if (msg.command == "cmd_success") { def.resolve(msg.result); } else { const error = new Error(msg.error.message); @@ -72,126 +57,106 @@ } }); } + /** * Attempt to connect to the database. This can fail if the user does not * grant permission. - * @return {Promise} Resolves if successfully connected. + * @returns Promise which resolves if successfully connected. */ - - connect() { - return this.ensureStarted().then(() => this.doCmd('connect')); + return this.ensureStarted().then(() => this.doCmd("connect")); } + /** * Clear the entire database. This should be used when logging out of a client * to prevent mixing data between accounts. - * @return {Promise} Resolved when the database is cleared. + * @returns Resolved when the database is cleared. */ - - clearDatabase() { - return this.ensureStarted().then(() => this.doCmd('clearDatabase')); + return this.ensureStarted().then(() => this.doCmd("clearDatabase")); } - /** @return {Promise} whether or not the database was newly created in this session. */ - + /** @returns whether or not the database was newly created in this session. */ isNewlyCreated() { - return this.doCmd('isNewlyCreated'); + return this.doCmd("isNewlyCreated"); } + /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ - - getSavedSync() { - return this.doCmd('getSavedSync'); + return this.doCmd("getSavedSync"); } - getNextBatchToken() { - return this.doCmd('getNextBatchToken'); + return this.doCmd("getNextBatchToken"); } - setSyncData(syncData) { - return this.doCmd('setSyncData', [syncData]); + return this.doCmd("setSyncData", [syncData]); } - syncToDatabase(userTuples) { - return this.doCmd('syncToDatabase', [userTuples]); + return this.doCmd("syncToDatabase", [userTuples]); } + /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ - - getOutOfBandMembers(roomId) { - return this.doCmd('getOutOfBandMembers', [roomId]); + return this.doCmd("getOutOfBandMembers", [roomId]); } + /** * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ - - setOutOfBandMembers(roomId, membershipEvents) { - return this.doCmd('setOutOfBandMembers', [roomId, membershipEvents]); + return this.doCmd("setOutOfBandMembers", [roomId, membershipEvents]); } - clearOutOfBandMembers(roomId) { - return this.doCmd('clearOutOfBandMembers', [roomId]); + return this.doCmd("clearOutOfBandMembers", [roomId]); } - getClientOptions() { - return this.doCmd('getClientOptions'); + return this.doCmd("getClientOptions"); } - storeClientOptions(options) { - return this.doCmd('storeClientOptions', [options]); + return this.doCmd("storeClientOptions", [options]); } + /** * Load all user presence events from the database. This is not cached. - * @return {Promise} A list of presence events in their raw form. + * @returns A list of presence events in their raw form. */ - - getUserPresenceEvents() { - return this.doCmd('getUserPresenceEvents'); + return this.doCmd("getUserPresenceEvents"); } - async saveToDeviceBatches(batches) { - return this.doCmd('saveToDeviceBatches', [batches]); + return this.doCmd("saveToDeviceBatches", [batches]); } - async getOldestToDeviceBatch() { - return this.doCmd('getOldestToDeviceBatch'); + return this.doCmd("getOldestToDeviceBatch"); } - async removeToDeviceBatch(id) { - return this.doCmd('removeToDeviceBatch', [id]); + return this.doCmd("removeToDeviceBatch", [id]); } - ensureStarted() { - if (this.startPromise === null) { + if (!this.startPromise) { this.worker = this.workerFactory(); - this.worker.onmessage = this.onWorkerMessage; // tell the worker the db name. + this.worker.onmessage = this.onWorkerMessage; - this.startPromise = this.doCmd('_setupWorker', [this.dbName]).then(() => { + // tell the worker the db name. + this.startPromise = this.doCmd("setupWorker", [this.dbName]).then(() => { _logger.logger.log("IndexedDB worker is ready"); }); } - return this.startPromise; } - doCmd(command, args) { // wrap in a q so if the postMessage throws, // the promise automatically gets rejected @@ -199,7 +164,7 @@ const seq = this.nextSeq++; const def = (0, _utils.defer)(); this.inFlight[seq] = def; - this.worker.postMessage({ + this.worker?.postMessage({ command, seq, args @@ -207,7 +172,5 @@ return def.promise; }); } - } - exports.RemoteIndexedDBStoreBackend = RemoteIndexedDBStoreBackend; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-store-worker.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-store-worker.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-store-worker.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/indexeddb-store-worker.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,23 +4,23 @@ value: true }); exports.IndexedDBStoreWorker = void 0; - var _indexeddbLocalBackend = require("./indexeddb-local-backend"); - var _logger = require("../logger"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** * This class lives in the webworker and drives a LocalIndexedDBStoreBackend * controlled by messages from the main process. * + * @example * It should be instantiated by a web worker script provided by the application * in a script, for example: - * + * ``` * import {IndexedDBStoreWorker} from 'matrix-js-sdk/lib/indexeddb-worker.js'; * const remoteWorker = new IndexedDBStoreWorker(postMessage); * onmessage = remoteWorker.onMessage; + * ``` * * Note that it is advisable to import this class by referencing the file directly to * avoid a dependency on the whole js-sdk. @@ -28,112 +28,90 @@ */ class IndexedDBStoreWorker { /** - * @param {function} postMessage The web worker postMessage function that + * @param postMessage - The web worker postMessage function that * should be used to communicate back to the main script. */ constructor(postMessage) { this.postMessage = postMessage; - - _defineProperty(this, "backend", null); - + _defineProperty(this, "backend", void 0); _defineProperty(this, "onMessage", ev => { const msg = ev.data; let prom; - switch (msg.command) { - case '_setupWorker': + case "setupWorker": // this is the 'indexedDB' global (where global != window // because it's a web worker and there is no window). this.backend = new _indexeddbLocalBackend.LocalIndexedDBStoreBackend(indexedDB, msg.args[0]); prom = Promise.resolve(); break; - - case 'connect': - prom = this.backend.connect(); + case "connect": + prom = this.backend?.connect(); break; - - case 'isNewlyCreated': - prom = this.backend.isNewlyCreated(); + case "isNewlyCreated": + prom = this.backend?.isNewlyCreated(); break; - - case 'clearDatabase': - prom = this.backend.clearDatabase(); + case "clearDatabase": + prom = this.backend?.clearDatabase(); break; - - case 'getSavedSync': - prom = this.backend.getSavedSync(false); + case "getSavedSync": + prom = this.backend?.getSavedSync(false); break; - - case 'setSyncData': - prom = this.backend.setSyncData(msg.args[0]); + case "setSyncData": + prom = this.backend?.setSyncData(msg.args[0]); break; - - case 'syncToDatabase': - prom = this.backend.syncToDatabase(msg.args[0]); + case "syncToDatabase": + prom = this.backend?.syncToDatabase(msg.args[0]); break; - - case 'getUserPresenceEvents': - prom = this.backend.getUserPresenceEvents(); + case "getUserPresenceEvents": + prom = this.backend?.getUserPresenceEvents(); break; - - case 'getNextBatchToken': - prom = this.backend.getNextBatchToken(); + case "getNextBatchToken": + prom = this.backend?.getNextBatchToken(); break; - - case 'getOutOfBandMembers': - prom = this.backend.getOutOfBandMembers(msg.args[0]); + case "getOutOfBandMembers": + prom = this.backend?.getOutOfBandMembers(msg.args[0]); break; - - case 'clearOutOfBandMembers': - prom = this.backend.clearOutOfBandMembers(msg.args[0]); + case "clearOutOfBandMembers": + prom = this.backend?.clearOutOfBandMembers(msg.args[0]); break; - - case 'setOutOfBandMembers': - prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]); + case "setOutOfBandMembers": + prom = this.backend?.setOutOfBandMembers(msg.args[0], msg.args[1]); break; - - case 'getClientOptions': - prom = this.backend.getClientOptions(); + case "getClientOptions": + prom = this.backend?.getClientOptions(); break; - - case 'storeClientOptions': - prom = this.backend.storeClientOptions(msg.args[0]); + case "storeClientOptions": + prom = this.backend?.storeClientOptions(msg.args[0]); break; - - case 'saveToDeviceBatches': - prom = this.backend.saveToDeviceBatches(msg.args[0]); + case "saveToDeviceBatches": + prom = this.backend?.saveToDeviceBatches(msg.args[0]); break; - - case 'getOldestToDeviceBatch': - prom = this.backend.getOldestToDeviceBatch(); + case "getOldestToDeviceBatch": + prom = this.backend?.getOldestToDeviceBatch(); break; - - case 'removeToDeviceBatch': - prom = this.backend.removeToDeviceBatch(msg.args[0]); + case "removeToDeviceBatch": + prom = this.backend?.removeToDeviceBatch(msg.args[0]); break; } - if (prom === undefined) { this.postMessage({ - command: 'cmd_fail', + command: "cmd_fail", seq: msg.seq, // Can't be an Error because they're not structured cloneable error: "Unrecognised command" }); return; } - prom.then(ret => { this.postMessage.call(null, { - command: 'cmd_success', + command: "cmd_success", seq: msg.seq, result: ret }); }, err => { _logger.logger.error("Error running command: " + msg.command, err); - this.postMessage.call(null, { - command: 'cmd_fail', + command: "cmd_fail", seq: msg.seq, // Just send a string because Error objects aren't cloneable error: { @@ -144,14 +122,12 @@ }); }); } + /** * Passes a message event from the main script into the class. This method * can be directly assigned to the web worker `onmessage` variable. * - * @param {Object} ev The message event + * @param ev - The message event */ - - } - exports.IndexedDBStoreWorker = IndexedDBStoreWorker; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/local-storage-events-emitter.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/local-storage-events-emitter.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/local-storage-events-emitter.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/local-storage-events-emitter.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,7 @@ value: true }); exports.localStorageErrorsEventsEmitter = exports.LocalStorageErrors = void 0; - var _typedEventEmitter = require("../models/typed-event-emitter"); - /* Copyright 2021 The Matrix.org Foundation C.I.C. @@ -24,7 +22,6 @@ */ let LocalStorageErrors; exports.LocalStorageErrors = LocalStorageErrors; - (function (LocalStorageErrors) { LocalStorageErrors["Global"] = "Global"; LocalStorageErrors["SetItemError"] = "setItem"; @@ -33,7 +30,6 @@ LocalStorageErrors["ClearError"] = "clear"; LocalStorageErrors["QuotaExceededError"] = "QuotaExceededError"; })(LocalStorageErrors || (exports.LocalStorageErrors = LocalStorageErrors = {})); - /** * Used in element-web as a temporary hack to handle all the localStorage errors on the highest level possible * As of 15.11.2021 (DD/MM/YYYY) we're not properly handling local storage exceptions anywhere. @@ -43,6 +39,5 @@ * See: https://github.com/vector-im/element-web/issues/18423 */ class LocalStorageErrorsEventsEmitter extends _typedEventEmitter.TypedEventEmitter {} - const localStorageErrorsEventsEmitter = new LocalStorageErrorsEventsEmitter(); exports.localStorageErrorsEventsEmitter = localStorageErrorsEventsEmitter; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/memory.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/memory.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/memory.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/memory.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,298 +4,240 @@ value: true }); exports.MemoryStore = void 0; - var _user = require("../models/user"); - var _roomState = require("../models/room-state"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +var _utils = require("../utils"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function isValidFilterId(filterId) { - const isValidStr = typeof filterId === "string" && !!filterId && filterId !== "undefined" && // exclude these as we've serialized undefined in localStorage before + const isValidStr = typeof filterId === "string" && !!filterId && filterId !== "undefined" && + // exclude these as we've serialized undefined in localStorage before filterId !== "null"; return isValidStr || typeof filterId === "number"; } - -/** - * Construct a new in-memory data store for the Matrix Client. - * @constructor - * @param {Object=} opts Config options - * @param {Storage} opts.localStorage The local storage instance to persist - * some forms of data such as tokens. Rooms will NOT be stored. - */ class MemoryStore { // roomId: Room // userId: User + // userId: { // filterId: Filter // } - // type : content + + // type: content + // roomId: [member events] + + /** + * Construct a new in-memory data store for the Matrix Client. + * @param opts - Config options + */ constructor(opts = {}) { _defineProperty(this, "rooms", {}); - _defineProperty(this, "users", {}); - _defineProperty(this, "syncToken", null); - - _defineProperty(this, "filters", {}); - - _defineProperty(this, "accountData", {}); - + _defineProperty(this, "filters", new _utils.MapWithDefault(() => new Map())); + _defineProperty(this, "accountData", new Map()); _defineProperty(this, "localStorage", void 0); - - _defineProperty(this, "oobMembers", {}); - + _defineProperty(this, "oobMembers", new Map()); _defineProperty(this, "pendingEvents", {}); - - _defineProperty(this, "clientOptions", {}); - + _defineProperty(this, "clientOptions", void 0); _defineProperty(this, "pendingToDeviceBatches", []); - _defineProperty(this, "nextToDeviceBatchId", 0); - _defineProperty(this, "onRoomMember", (event, state, member) => { if (member.membership === "invite") { // We do NOT add invited members because people love to typo user IDs // which would then show up in these lists (!) return; } - const user = this.users[member.userId] || new _user.User(member.userId); - if (member.name) { user.setDisplayName(member.name); - if (member.events.member) { user.setRawDisplayName(member.events.member.getDirectionalContent().displayname); } } - if (member.events.member && member.events.member.getContent().avatar_url) { user.setAvatarUrl(member.events.member.getContent().avatar_url); } - this.users[user.userId] = user; }); - this.localStorage = opts.localStorage; } + /** * Retrieve the token to stream from. - * @return {string} The token or null. + * @returns The token or null. */ - - getSyncToken() { return this.syncToken; } - /** @return {Promise} whether or not the database was newly created in this session. */ - + /** @returns whether or not the database was newly created in this session. */ isNewlyCreated() { return Promise.resolve(true); } + /** * Set the token to stream from. - * @param {string} token The token to stream from. + * @param token - The token to stream from. */ - - setSyncToken(token) { this.syncToken = token; } + /** * Store the given room. - * @param {Room} room The room to be stored. All properties must be stored. + * @param room - The room to be stored. All properties must be stored. */ - - storeRoom(room) { - this.rooms[room.roomId] = room; // add listeners for room member changes so we can keep the room member + this.rooms[room.roomId] = room; + // add listeners for room member changes so we can keep the room member // map up-to-date. - - room.currentState.on(_roomState.RoomStateEvent.Members, this.onRoomMember); // add existing members - + room.currentState.on(_roomState.RoomStateEvent.Members, this.onRoomMember); + // add existing members room.currentState.getMembers().forEach(m => { this.onRoomMember(null, room.currentState, m); }); } + /** * Called when a room member in a room being tracked by this store has been * updated. - * @param {MatrixEvent} event - * @param {RoomState} state - * @param {RoomMember} member */ - /** * Retrieve a room by its' room ID. - * @param {string} roomId The room ID. - * @return {Room} The room or null. + * @param roomId - The room ID. + * @returns The room or null. */ getRoom(roomId) { return this.rooms[roomId] || null; } + /** * Retrieve all known rooms. - * @return {Room[]} A list of rooms, which may be empty. + * @returns A list of rooms, which may be empty. */ - - getRooms() { return Object.values(this.rooms); } + /** * Permanently delete a room. - * @param {string} roomId */ - - removeRoom(roomId) { if (this.rooms[roomId]) { this.rooms[roomId].currentState.removeListener(_roomState.RoomStateEvent.Members, this.onRoomMember); } - delete this.rooms[roomId]; } + /** * Retrieve a summary of all the rooms. - * @return {RoomSummary[]} A summary of each room. + * @returns A summary of each room. */ - - getRoomSummaries() { return Object.values(this.rooms).map(function (room) { return room.summary; }); } + /** * Store a User. - * @param {User} user The user to store. + * @param user - The user to store. */ - - storeUser(user) { this.users[user.userId] = user; } + /** * Retrieve a User by its' user ID. - * @param {string} userId The user ID. - * @return {User} The user or null. + * @param userId - The user ID. + * @returns The user or null. */ - - getUser(userId) { return this.users[userId] || null; } + /** * Retrieve all known users. - * @return {User[]} A list of users, which may be empty. + * @returns A list of users, which may be empty. */ - - getUsers() { return Object.values(this.users); } + /** * Retrieve scrollback for this room. - * @param {Room} room The matrix room - * @param {number} limit The max number of old events to retrieve. - * @return {Array} An array of objects which will be at most 'limit' + * @param room - The matrix room + * @param limit - The max number of old events to retrieve. + * @returns An array of objects which will be at most 'limit' * length and at least 0. The objects are the raw event JSON. */ - - scrollback(room, limit) { return []; } + /** * Store events for a room. The events have already been added to the timeline - * @param {Room} room The room to store events for. - * @param {Array} events The events to store. - * @param {string} token The token associated with these events. - * @param {boolean} toStart True if these are paginated results. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param toStart - True if these are paginated results. */ - - - storeEvents(room, events, token, toStart) {// no-op because they've already been added to the room instance. + storeEvents(room, events, token, toStart) { + // no-op because they've already been added to the room instance. } + /** * Store a filter. - * @param {Filter} filter */ - - storeFilter(filter) { - if (!filter) { - return; - } - - if (!this.filters[filter.userId]) { - this.filters[filter.userId] = {}; - } - - this.filters[filter.userId][filter.filterId] = filter; + if (!filter?.userId || !filter?.filterId) return; + this.filters.getOrCreate(filter.userId).set(filter.filterId, filter); } + /** * Retrieve a filter. - * @param {string} userId - * @param {string} filterId - * @return {?Filter} A filter or null. + * @returns A filter or null. */ - - getFilter(userId, filterId) { - if (!this.filters[userId] || !this.filters[userId][filterId]) { - return null; - } - - return this.filters[userId][filterId]; + return this.filters.get(userId)?.get(filterId) || null; } + /** * Retrieve a filter ID with the given name. - * @param {string} filterName The filter name. - * @return {?string} The filter ID or null. + * @param filterName - The filter name. + * @returns The filter ID or null. */ - - getFilterIdByName(filterName) { if (!this.localStorage) { return null; } - - const key = "mxjssdk_memory_filter_" + filterName; // XXX Storage.getItem doesn't throw ... + const key = "mxjssdk_memory_filter_" + filterName; + // XXX Storage.getItem doesn't throw ... // or are we using something different // than window.localStorage in some cases // that does throw? // that would be very naughty - try { const value = this.localStorage.getItem(key); - if (isValidFilterId(value)) { return value; } } catch (e) {} - return null; } + /** * Set a filter name to ID mapping. - * @param {string} filterName - * @param {string} filterId */ - - setFilterIdByName(filterName, filterId) { if (!this.localStorage) { return; } - const key = "mxjssdk_memory_filter_" + filterName; - try { if (isValidFilterId(filterId)) { this.localStorage.setItem(key, filterId); @@ -304,155 +246,140 @@ } } catch (e) {} } + /** * Store user-scoped account data events. * N.B. that account data only allows a single event per type, so multiple * events with the same type will replace each other. - * @param {Array} events The events to store. + * @param events - The events to store. */ - - storeAccountDataEvents(events) { events.forEach(event => { - this.accountData[event.getType()] = event; + // MSC3391: an event with content of {} should be interpreted as deleted + const isDeleted = !Object.keys(event.getContent()).length; + if (isDeleted) { + this.accountData.delete(event.getType()); + } else { + this.accountData.set(event.getType(), event); + } }); } + /** * Get account data event by event type - * @param {string} eventType The event type being queried - * @return {?MatrixEvent} the user account_data event of given type, if any + * @param eventType - The event type being queried + * @returns the user account_data event of given type, if any */ - - getAccountData(eventType) { - return this.accountData[eventType]; + return this.accountData.get(eventType); } + /** * setSyncData does nothing as there is no backing data store. * - * @param {Object} syncData The sync data - * @return {Promise} An immediately resolved promise. + * @param syncData - The sync data + * @returns An immediately resolved promise. */ - - setSyncData(syncData) { return Promise.resolve(); } + /** * We never want to save becase we have nothing to save to. * - * @return {boolean} If the store wants to save + * @returns If the store wants to save */ - - wantsSave() { return false; } + /** * Save does nothing as there is no backing data store. - * @param {bool} force True to force a save (but the memory + * @param force - True to force a save (but the memory * store still can't save anything) */ - - save(force) {} + /** * Startup does nothing as this store doesn't require starting up. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ - - startup() { return Promise.resolve(); } + /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ - - getSavedSync() { return Promise.resolve(null); } + /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ - - getSavedSyncToken() { return Promise.resolve(null); } + /** * Delete all data from this store. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ - - deleteAllData() { - this.rooms = {// roomId: Room + this.rooms = { + // roomId: Room }; - this.users = {// userId: User + this.users = { + // userId: User }; this.syncToken = null; - this.filters = {// userId: { - // filterId: Filter - // } - }; - this.accountData = {// type : content - }; + this.filters = new _utils.MapWithDefault(() => new Map()); + this.accountData = new Map(); // type : content return Promise.resolve(); } + /** * Returns the out-of-band membership events for this room that * were previously loaded. - * @param {string} roomId - * @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members - * @returns {null} in case the members for this room haven't been stored yet + * @returns the events, potentially an empty array if OOB loading didn't yield any new members + * @returns in case the members for this room haven't been stored yet */ - - getOutOfBandMembers(roomId) { - return Promise.resolve(this.oobMembers[roomId] || null); + return Promise.resolve(this.oobMembers.get(roomId) || null); } + /** * Stores the out-of-band membership events for this room. Note that * it still makes sense to store an empty array as the OOB status for the room is * marked as fetched, and getOutOfBandMembers will return an empty array instead of null - * @param {string} roomId - * @param {event[]} membershipEvents the membership events to store - * @returns {Promise} when all members have been stored + * @param membershipEvents - the membership events to store + * @returns when all members have been stored */ - - setOutOfBandMembers(roomId, membershipEvents) { - this.oobMembers[roomId] = membershipEvents; + this.oobMembers.set(roomId, membershipEvents); return Promise.resolve(); } - clearOutOfBandMembers(roomId) { - this.oobMembers = {}; + this.oobMembers.delete(roomId); return Promise.resolve(); } - getClientOptions() { return Promise.resolve(this.clientOptions); } - storeClientOptions(options) { this.clientOptions = Object.assign({}, options); return Promise.resolve(); } - async getPendingEvents(roomId) { return this.pendingEvents[roomId] ?? []; } - async setPendingEvents(roomId, events) { this.pendingEvents[roomId] = events; } - saveToDeviceBatches(batches) { for (const batch of batches) { this.pendingToDeviceBatches.push({ @@ -462,20 +389,15 @@ batch: batch.batch }); } - return Promise.resolve(); } - async getOldestToDeviceBatch() { if (this.pendingToDeviceBatches.length === 0) return null; return this.pendingToDeviceBatches[0]; } - removeToDeviceBatch(id) { this.pendingToDeviceBatches = this.pendingToDeviceBatches.filter(batch => batch.id !== id); return Promise.resolve(); } - } - exports.MemoryStore = MemoryStore; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/stub.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/stub.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/store/stub.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/store/stub.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,9 @@ value: true }); exports.StubStore = void 0; - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. @@ -25,291 +25,232 @@ /** * This is an internal module. - * @module store/stub */ /** * Construct a stub store. This does no-ops on most store methods. - * @constructor */ class StubStore { constructor() { - _defineProperty(this, "accountData", {}); - + _defineProperty(this, "accountData", new Map()); _defineProperty(this, "fromToken", null); } - - /** @return {Promise} whether or not the database was newly created in this session. */ + /** @returns whether or not the database was newly created in this session. */ isNewlyCreated() { return Promise.resolve(true); } + /** * Get the sync token. - * @return {string} */ - - getSyncToken() { return this.fromToken; } + /** * Set the sync token. - * @param {string} token */ - - setSyncToken(token) { this.fromToken = token; } + /** * No-op. - * @param {Room} room */ - - storeRoom(room) {} + /** * No-op. - * @param {string} roomId - * @return {null} */ - - getRoom(roomId) { return null; } + /** * No-op. - * @return {Array} An empty array. + * @returns An empty array. */ - - getRooms() { return []; } + /** * Permanently delete a room. - * @param {string} roomId */ - - removeRoom(roomId) { return; } + /** * No-op. - * @return {Array} An empty array. + * @returns An empty array. */ - - getRoomSummaries() { return []; } + /** * No-op. - * @param {User} user */ - - storeUser(user) {} + /** * No-op. - * @param {string} userId - * @return {null} */ - - getUser(userId) { return null; } + /** * No-op. - * @return {User[]} */ - - getUsers() { return []; } + /** * No-op. - * @param {Room} room - * @param {number} limit - * @return {Array} */ - - scrollback(room, limit) { return []; } + /** * Store events for a room. - * @param {Room} room The room to store events for. - * @param {Array} events The events to store. - * @param {string} token The token associated with these events. - * @param {boolean} toStart True if these are paginated results. + * @param room - The room to store events for. + * @param events - The events to store. + * @param token - The token associated with these events. + * @param toStart - True if these are paginated results. */ - - storeEvents(room, events, token, toStart) {} + /** * Store a filter. - * @param {Filter} filter */ - - storeFilter(filter) {} + /** * Retrieve a filter. - * @param {string} userId - * @param {string} filterId - * @return {?Filter} A filter or null. + * @returns A filter or null. */ - - getFilter(userId, filterId) { return null; } + /** * Retrieve a filter ID with the given name. - * @param {string} filterName The filter name. - * @return {?string} The filter ID or null. + * @param filterName - The filter name. + * @returns The filter ID or null. */ - - getFilterIdByName(filterName) { return null; } + /** * Set a filter name to ID mapping. - * @param {string} filterName - * @param {string} filterId */ - - setFilterIdByName(filterName, filterId) {} + /** * Store user-scoped account data events - * @param {Array} events The events to store. + * @param events - The events to store. */ - - storeAccountDataEvents(events) {} + /** * Get account data event by event type - * @param {string} eventType The event type being queried + * @param eventType - The event type being queried */ - - getAccountData(eventType) { return undefined; } + /** * setSyncData does nothing as there is no backing data store. * - * @param {Object} syncData The sync data - * @return {Promise} An immediately resolved promise. + * @param syncData - The sync data + * @returns An immediately resolved promise. */ - - setSyncData(syncData) { return Promise.resolve(); } + /** * We never want to save because we have nothing to save to. * - * @return {boolean} If the store wants to save + * @returns If the store wants to save */ - - wantsSave() { return false; } + /** * Save does nothing as there is no backing data store. */ - - save() {} + /** * Startup does nothing. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ - - startup() { return Promise.resolve(); } + /** - * @return {Promise} Resolves with a sync response to restore the + * @returns Promise which resolves with a sync response to restore the * client state to where it was at the last save, or null if there * is no saved sync data. */ - - getSavedSync() { return Promise.resolve(null); } + /** - * @return {Promise} If there is a saved sync, the nextBatch token + * @returns If there is a saved sync, the nextBatch token * for this sync, otherwise null. */ - - getSavedSyncToken() { return Promise.resolve(null); } + /** * Delete all data from this store. Does nothing since this store * doesn't store anything. - * @return {Promise} An immediately resolved promise. + * @returns An immediately resolved promise. */ - - deleteAllData() { return Promise.resolve(); } - getOutOfBandMembers() { return Promise.resolve(null); } - setOutOfBandMembers(roomId, membershipEvents) { return Promise.resolve(); } - clearOutOfBandMembers() { return Promise.resolve(); } - getClientOptions() { - return Promise.resolve({}); + return Promise.resolve(undefined); } - storeClientOptions(options) { return Promise.resolve(); } - async getPendingEvents(roomId) { return []; } - setPendingEvents(roomId, events) { return Promise.resolve(); } - async saveToDeviceBatches(batch) { return Promise.resolve(); } - getOldestToDeviceBatch() { return Promise.resolve(null); } - async removeToDeviceBatch(id) { return Promise.resolve(); } - } - exports.StubStore = StubStore; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/sync-accumulator.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/sync-accumulator.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/sync-accumulator.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/sync-accumulator.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,22 +4,27 @@ value: true }); exports.SyncAccumulator = exports.Category = void 0; - var _logger = require("./logger"); - var _utils = require("./utils"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +var _event = require("./@types/event"); +var _read_receipts = require("./@types/read_receipts"); +var _sync = require("./@types/sync"); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /* eslint-enable camelcase */ let Category; exports.Category = Category; - (function (Category) { Category["Invite"] = "invite"; Category["Leave"] = "leave"; Category["Join"] = "join"; })(Category || (exports.Category = Category = {})); +function isTaggedEvent(event) { + return "_localTs" in event && event["_localTs"] !== undefined; +} /** * The purpose of this class is to accumulate /sync responses such that a @@ -34,81 +39,60 @@ class SyncAccumulator { // $event_type: Object // $roomId: { ... sync 'invite' json data ... } + // the /sync token which corresponds to the last time rooms were // accumulated. We remember this so that any caller can obtain a // coherent /sync response and know at what point they should be // streaming from without losing events. - /** - * @param {Object} opts - * @param {Number=} opts.maxTimelineEntries The ideal maximum number of - * timeline entries to keep in the sync response. This is best-effort, as - * clients do not always have a back-pagination token for each event, so - * it's possible there may be slightly *less* than this value. There will - * never be more. This cannot be 0 or else it makes it impossible to scroll - * back in a room. Default: 50. - */ constructor(opts = {}) { this.opts = opts; - _defineProperty(this, "accountData", {}); - _defineProperty(this, "inviteRooms", {}); - _defineProperty(this, "joinRooms", {}); - _defineProperty(this, "nextBatch", null); - this.opts.maxTimelineEntries = this.opts.maxTimelineEntries || 50; } - accumulate(syncResponse, fromDatabase = false) { this.accumulateRooms(syncResponse, fromDatabase); this.accumulateAccountData(syncResponse); this.nextBatch = syncResponse.next_batch; } - accumulateAccountData(syncResponse) { if (!syncResponse.account_data || !syncResponse.account_data.events) { return; - } // Clobbers based on event type. - - + } + // Clobbers based on event type. syncResponse.account_data.events.forEach(e => { this.accountData[e.type] = e; }); } + /** * Accumulate incremental /sync room data. - * @param {Object} syncResponse the complete /sync JSON - * @param {boolean} fromDatabase True if the sync response is one saved to the database + * @param syncResponse - the complete /sync JSON + * @param fromDatabase - True if the sync response is one saved to the database */ - - accumulateRooms(syncResponse, fromDatabase = false) { if (!syncResponse.rooms) { return; } - if (syncResponse.rooms.invite) { Object.keys(syncResponse.rooms.invite).forEach(roomId => { this.accumulateRoom(roomId, Category.Invite, syncResponse.rooms.invite[roomId], fromDatabase); }); } - if (syncResponse.rooms.join) { Object.keys(syncResponse.rooms.join).forEach(roomId => { this.accumulateRoom(roomId, Category.Join, syncResponse.rooms.join[roomId], fromDatabase); }); } - if (syncResponse.rooms.leave) { Object.keys(syncResponse.rooms.leave).forEach(roomId => { this.accumulateRoom(roomId, Category.Leave, syncResponse.rooms.leave[roomId], fromDatabase); }); } } - accumulateRoom(roomId, category, data, fromDatabase = false) { // Valid /sync state transitions // +--------+ <======+ 1: Accept an invite @@ -126,7 +110,6 @@ // (5) this.accumulateInviteState(roomId, data); break; - case Category.Join: if (this.inviteRooms[roomId]) { // (1) @@ -134,12 +117,10 @@ // the entire state and timeline on 'join', so delete previous // invite state delete this.inviteRooms[roomId]; - } // (3) - - + } + // (3) this.accumulateJoinState(roomId, data, fromDatabase); break; - case Category.Leave: if (this.inviteRooms[roomId]) { // (4) @@ -148,52 +129,42 @@ // (2) delete this.joinRooms[roomId]; } - break; - default: _logger.logger.error("Unknown cateogory: ", category); - } } - accumulateInviteState(roomId, data) { if (!data.invite_state || !data.invite_state.events) { // no new data return; } - if (!this.inviteRooms[roomId]) { this.inviteRooms[roomId] = { invite_state: data.invite_state }; return; - } // accumulate extra keys for invite->invite transitions + } + // accumulate extra keys for invite->invite transitions // clobber based on event type / state key // We expect invite_state to be small, so just loop over the events - - const currentData = this.inviteRooms[roomId]; data.invite_state.events.forEach(e => { let hasAdded = false; - for (let i = 0; i < currentData.invite_state.events.length; i++) { const current = currentData.invite_state.events[i]; - if (current.type === e.type && current.state_key == e.state_key) { currentData.invite_state.events[i] = e; // update - hasAdded = true; } } - if (!hasAdded) { currentData.invite_state.events.push(e); } }); - } // Accumulate timeline and state events in a room. - + } + // Accumulate timeline and state events in a room. accumulateJoinState(roomId, data, fromDatabase = false) { // We expect this function to be called a lot (every /sync) so we want // this to be fast. /sync stores events in an array but we often want @@ -201,6 +172,7 @@ // maps all the time, just keep private maps which contain // the actual current accumulated sync state, and array-ify it when // getJSON() is called. + // State resolution: // The 'state' key is the delta from the previous sync (or start of time // if no token was supplied), to the START of the timeline. To obtain @@ -214,6 +186,7 @@ // // When getJSON() is called, we 'roll back' the current state by the // number of entries in the timeline to work out what 'state' should be. + // Back-pagination: // On an initial /sync, the server provides a back-pagination token for // the start of the timeline. When /sync deltas come down, they also @@ -227,6 +200,7 @@ // opts.maxTimelineEntries, and we may have a few less. We should never // have more though, provided that the /sync limit is less than or equal // to opts.maxTimelineEntries. + if (!this.joinRooms[roomId]) { // Create truly empty objects so event types of 'hasOwnProperty' and co // don't cause this code to break. @@ -235,25 +209,25 @@ _timeline: [], _accountData: Object.create(null), _unreadNotifications: {}, + _unreadThreadNotifications: {}, _summary: {}, - _readReceipts: {} + _readReceipts: {}, + _threadReadReceipts: {} }; } - const currentData = this.joinRooms[roomId]; - if (data.account_data && data.account_data.events) { // clobber based on type data.account_data.events.forEach(e => { currentData._accountData[e.type] = e; }); - } // these probably clobber, spec is unclear. - + } + // these probably clobber, spec is unclear. if (data.unread_notifications) { currentData._unreadNotifications = data.unread_notifications; } - + currentData._unreadThreadNotifications = data[_sync.UNREAD_THREAD_NOTIFICATIONS.stable] ?? data[_sync.UNREAD_THREAD_NOTIFICATIONS.unstable] ?? undefined; if (data.summary) { const HEROES_KEY = "m.heroes"; const INVITED_COUNT_KEY = "m.invited_member_count"; @@ -264,98 +238,93 @@ acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] || acc[JOINED_COUNT_KEY]; acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] || acc[INVITED_COUNT_KEY]; } - - if (data.ephemeral && data.ephemeral.events) { - data.ephemeral.events.forEach(e => { - // We purposefully do not persist m.typing events. - // Technically you could refresh a browser before the timer on a - // typing event is up, so it'll look like you aren't typing when - // you really still are. However, the alternative is worse. If - // we do persist typing events, it will look like people are - // typing forever until someone really does start typing (which - // will prompt Synapse to send down an actual m.typing event to - // clobber the one we persisted). - if (e.type !== "m.receipt" || !e.content) { - // This means we'll drop unknown ephemeral events but that - // seems okay. - return; - } // Handle m.receipt events. They clobber based on: - // (user_id, receipt_type) - // but they are keyed in the event as: - // content:{ $event_id: { $receipt_type: { $user_id: {json} }}} - // so store them in the former so we can accumulate receipt deltas - // quickly and efficiently (we expect a lot of them). Fold the - // receipt type into the key name since we only have 1 at the - // moment (m.read) and nested JSON objects are slower and more - // of a hassle to work with. We'll inflate this back out when - // getJSON() is called. - - - Object.keys(e.content).forEach(eventId => { - Object.entries(e.content[eventId]).forEach(([key, value]) => { - if (!(0, _utils.isSupportedReceiptType)(key)) return; - Object.keys(value).forEach(userId => { - // clobber on user ID - currentData._readReceipts[userId] = { - data: e.content[eventId][key][userId], - type: key, - eventId: eventId - }; - }); - }); + data.ephemeral?.events?.forEach(e => { + // We purposefully do not persist m.typing events. + // Technically you could refresh a browser before the timer on a + // typing event is up, so it'll look like you aren't typing when + // you really still are. However, the alternative is worse. If + // we do persist typing events, it will look like people are + // typing forever until someone really does start typing (which + // will prompt Synapse to send down an actual m.typing event to + // clobber the one we persisted). + if (e.type !== _event.EventType.Receipt || !e.content) { + // This means we'll drop unknown ephemeral events but that + // seems okay. + return; + } + // Handle m.receipt events. They clobber based on: + // (user_id, receipt_type) + // but they are keyed in the event as: + // content:{ $event_id: { $receipt_type: { $user_id: {json} }}} + // so store them in the former so we can accumulate receipt deltas + // quickly and efficiently (we expect a lot of them). Fold the + // receipt type into the key name since we only have 1 at the + // moment (m.read) and nested JSON objects are slower and more + // of a hassle to work with. We'll inflate this back out when + // getJSON() is called. + Object.keys(e.content).forEach(eventId => { + Object.entries(e.content[eventId]).forEach(([key, value]) => { + if (!(0, _utils.isSupportedReceiptType)(key)) return; + for (const userId of Object.keys(value)) { + const data = e.content[eventId][key][userId]; + const receipt = { + data: e.content[eventId][key][userId], + type: key, + eventId: eventId + }; + if (!data.thread_id || data.thread_id === _read_receipts.MAIN_ROOM_TIMELINE) { + currentData._readReceipts[userId] = receipt; + } else { + currentData._threadReadReceipts = _objectSpread(_objectSpread({}, currentData._threadReadReceipts), {}, { + [data.thread_id]: _objectSpread(_objectSpread({}, currentData._threadReadReceipts[data.thread_id] ?? {}), {}, { + [userId]: receipt + }) + }); + } + } }); }); - } // if we got a limited sync, we need to remove all timeline entries or else - // we will have gaps in the timeline. - + }); + // if we got a limited sync, we need to remove all timeline entries or else + // we will have gaps in the timeline. if (data.timeline && data.timeline.limited) { currentData._timeline = []; - } // Work out the current state. The deltas need to be applied in the order: + } + + // Work out the current state. The deltas need to be applied in the order: // - existing state which didn't come down /sync. // - State events under the 'state' key. // - State events in the 'timeline'. - - - if (data.state && data.state.events) { - data.state.events.forEach(e => { - setState(currentData._currentState, e); - }); - } - - if (data.timeline && data.timeline.events) { - data.timeline.events.forEach((e, index) => { - // this nops if 'e' isn't a state event - setState(currentData._currentState, e); // append the event to the timeline. The back-pagination token - // corresponds to the first event in the timeline - - let transformedEvent; - - if (!fromDatabase) { - transformedEvent = Object.assign({}, e); - - if (transformedEvent.unsigned !== undefined) { - transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned); - } - - const age = e.unsigned ? e.unsigned.age : e.age; - if (age !== undefined) transformedEvent._localTs = Date.now() - age; - } else { - transformedEvent = e; + data.state?.events?.forEach(e => { + setState(currentData._currentState, e); + }); + data.timeline?.events?.forEach((e, index) => { + // this nops if 'e' isn't a state event + setState(currentData._currentState, e); + // append the event to the timeline. The back-pagination token + // corresponds to the first event in the timeline + let transformedEvent; + if (!fromDatabase) { + transformedEvent = Object.assign({}, e); + if (transformedEvent.unsigned !== undefined) { + transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned); } - - currentData._timeline.push({ - event: transformedEvent, - token: index === 0 ? data.timeline.prev_batch : null - }); + const age = e.unsigned ? e.unsigned.age : e.age; + if (age !== undefined) transformedEvent._localTs = Date.now() - age; + } else { + transformedEvent = e; + } + currentData._timeline.push({ + event: transformedEvent, + token: index === 0 ? data.timeline.prev_batch ?? null : null }); - } // attempt to prune the timeline by jumping between events which have - // pagination tokens. - + }); + // attempt to prune the timeline by jumping between events which have + // pagination tokens. if (currentData._timeline.length > this.opts.maxTimelineEntries) { const startIndex = currentData._timeline.length - this.opts.maxTimelineEntries; - for (let i = startIndex; i < currentData._timeline.length; i++) { if (currentData._timeline[i].token) { // keep all events after this, including this one @@ -365,13 +334,14 @@ } } } + /** * Return everything under the 'rooms' key from a /sync response which * represents all room data that should be stored. This should be paired * with the sync token which represents the most recent /sync response * provided to accumulate(). - * @param {boolean} forDatabase True to generate a sync to be saved to storage - * @return {Object} An object with a "nextBatch", "roomsData" and "accountData" + * @param forDatabase - True to generate a sync to be saved to storage + * @returns An object with a "nextBatch", "roomsData" and "accountData" * keys. * The "nextBatch" key is a string which represents at what point in the * /sync stream the accumulator reached. This token should be used when @@ -380,8 +350,6 @@ * /sync response from the 'rooms' key onwards. The "accountData" key is * a list of raw events which represent global account data. */ - - getJSON(forDatabase = false) { const data = { join: {}, @@ -419,38 +387,39 @@ prev_batch: null }, unread_notifications: roomData._unreadNotifications, + unread_thread_notifications: roomData._unreadThreadNotifications, summary: roomData._summary - }; // Add account data - + }; + // Add account data Object.keys(roomData._accountData).forEach(evType => { roomJson.account_data.events.push(roomData._accountData[evType]); - }); // Add receipt data + }); + // Add receipt data const receiptEvent = { - type: "m.receipt", + type: _event.EventType.Receipt, room_id: roomId, - content: {// $event_id: { "m.read": { $user_id: $json } } + content: { + // $event_id: { "m.read": { $user_id: $json } } } }; - Object.keys(roomData._readReceipts).forEach(userId => { - const receiptData = roomData._readReceipts[userId]; - - if (!receiptEvent.content[receiptData.eventId]) { - receiptEvent.content[receiptData.eventId] = {}; - } - - if (!receiptEvent.content[receiptData.eventId][receiptData.type]) { - receiptEvent.content[receiptData.eventId][receiptData.type] = {}; + const receiptEventContent = new _utils.MapWithDefault(() => new _utils.MapWithDefault(() => new Map())); + for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) { + receiptEventContent.getOrCreate(receiptData.eventId).getOrCreate(receiptData.type).set(userId, receiptData.data); + } + for (const threadReceipts of Object.values(roomData._threadReadReceipts)) { + for (const [userId, receiptData] of Object.entries(threadReceipts)) { + receiptEventContent.getOrCreate(receiptData.eventId).getOrCreate(receiptData.type).set(userId, receiptData.data); } + } + receiptEvent.content = (0, _utils.recursiveMapToObject)(receiptEventContent); - receiptEvent.content[receiptData.eventId][receiptData.type][userId] = receiptData.data; - }); // add only if we have some receipt data - - if (Object.keys(receiptEvent.content).length > 0) { + // add only if we have some receipt data + if (receiptEventContent.size > 0) { roomJson.ephemeral.events.push(receiptEvent); - } // Add timeline data - + } + // Add timeline data roomData._timeline.forEach(msgData => { if (!roomJson.timeline.prev_batch) { // the first event we add to the timeline MUST match up to @@ -461,11 +430,9 @@ roomJson.timeline.prev_batch = msgData.token; } - let transformedEvent; - - if (!forDatabase && msgData.event["_localTs"]) { - // This means we have to copy each event so we can fix it up to + if (!forDatabase && isTaggedEvent(msgData.event)) { + // This means we have to copy each event, so we can fix it up to // set a correct 'age' parameter whilst keeping the local timestamp // on our stored event. If this turns out to be a bottleneck, it could // be optimised either by doing this in the main process after the data @@ -474,66 +441,55 @@ // directly rather than turning it into age to then immediately be // transformed back again into a local timestamp. transformedEvent = Object.assign({}, msgData.event); - if (transformedEvent.unsigned !== undefined) { transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned); } - delete transformedEvent._localTs; transformedEvent.unsigned = transformedEvent.unsigned || {}; - transformedEvent.unsigned.age = Date.now() - msgData.event["_localTs"]; + transformedEvent.unsigned.age = Date.now() - msgData.event._localTs; } else { transformedEvent = msgData.event; } - roomJson.timeline.events.push(transformedEvent); - }); // Add state data: roll back current state to the start of timeline, + }); + + // Add state data: roll back current state to the start of timeline, // by "reverse clobbering" from the end of the timeline to the start. // Convert maps back into arrays. - - const rollBackState = Object.create(null); - for (let i = roomJson.timeline.events.length - 1; i >= 0; i--) { const timelineEvent = roomJson.timeline.events[i]; - if (timelineEvent.state_key === null || timelineEvent.state_key === undefined) { continue; // not a state event - } // since we're going back in time, we need to use the previous + } + // since we're going back in time, we need to use the previous // state value else we'll break causality. We don't have the // complete previous state event, so we need to create one. - - const prevStateEvent = (0, _utils.deepCopy)(timelineEvent); - if (prevStateEvent.unsigned) { if (prevStateEvent.unsigned.prev_content) { prevStateEvent.content = prevStateEvent.unsigned.prev_content; } - if (prevStateEvent.unsigned.prev_sender) { prevStateEvent.sender = prevStateEvent.unsigned.prev_sender; } } - setState(rollBackState, prevStateEvent); } - Object.keys(roomData._currentState).forEach(evType => { Object.keys(roomData._currentState[evType]).forEach(stateKey => { let ev = roomData._currentState[evType][stateKey]; - if (rollBackState[evType] && rollBackState[evType][stateKey]) { // use the reverse clobbered event instead. ev = rollBackState[evType][stateKey]; } - roomJson.state.events.push(ev); }); }); data.join[roomId] = roomJson; - }); // Add account data + }); + // Add account data const accData = []; Object.keys(this.accountData).forEach(evType => { accData.push(this.accountData[evType]); @@ -544,23 +500,17 @@ accountData: accData }; } - getNextBatchToken() { return this.nextBatch; } - } - exports.SyncAccumulator = SyncAccumulator; - function setState(eventMap, event) { if (event.state_key === null || event.state_key === undefined || !event.type) { return; } - if (!eventMap[event.type]) { eventMap[event.type] = Object.create(null); } - eventMap[event.type][event.state_key] = event; } \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/sync.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/sync.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/sync.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/sync.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,57 +5,46 @@ }); exports.SyncState = exports.SyncApi = void 0; exports._createAndReEmitRoom = _createAndReEmitRoom; - +exports.defaultClientOpts = defaultClientOpts; +exports.defaultSyncApiOpts = defaultSyncApiOpts; var _user = require("./models/user"); - var _room = require("./models/room"); - var utils = _interopRequireWildcard(require("./utils")); - var _filter = require("./filter"); - var _eventTimeline = require("./models/event-timeline"); - -var _pushprocessor = require("./pushprocessor"); - var _logger = require("./logger"); - var _errors = require("./errors"); - var _client = require("./client"); - var _httpApi = require("./http-api"); - var _event = require("./@types/event"); - var _roomState = require("./models/room-state"); - var _roomMember = require("./models/room-member"); - var _beacon = require("./models/beacon"); - +var _sync = require("./@types/sync"); +var _feature = require("./feature"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +const DEBUG = true; -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -const DEBUG = true; // /sync requests allow you to set a timeout= but the request may continue +// /sync requests allow you to set a timeout= but the request may continue // beyond that and wedge forever, so we need to track how long we are willing // to keep open the connection. This constant is *ADDED* to the timeout= value // to determine the max time we're willing to wait. +const BUFFER_PERIOD_MS = 80 * 1000; -const BUFFER_PERIOD_MS = 80 * 1000; // Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed +// Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed // to RECONNECTING. This is needed to inform the client of server issues when the // keepAlive is successful but the server /sync fails. - const FAILED_SYNC_ERROR_THRESHOLD = 3; let SyncState; // Room versions where "insertion", "batch", and "marker" events are controlled // by power-levels. MSC2716 is supported in existing room versions but they // should only have special meaning when the room creator sends them. - exports.SyncState = SyncState; - (function (SyncState) { SyncState["Error"] = "ERROR"; SyncState["Prepared"] = "PREPARED"; @@ -64,75 +53,73 @@ SyncState["Catchup"] = "CATCHUP"; SyncState["Reconnecting"] = "RECONNECTING"; })(SyncState || (exports.SyncState = SyncState = {})); - -const MSC2716_ROOM_VERSIONS = ['org.matrix.msc2716v3']; - +const MSC2716_ROOM_VERSIONS = ["org.matrix.msc2716v3"]; function getFilterName(userId, suffix) { // scope this on the user ID because people may login on many accounts // and they all need to be stored! return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : ""); } +/* istanbul ignore next */ function debuglog(...params) { if (!DEBUG) return; - _logger.logger.log(...params); } +/** + * Options passed into the constructor of SyncApi by MatrixClient + */ var SetPresence; - (function (SetPresence) { SetPresence["Offline"] = "offline"; SetPresence["Online"] = "online"; SetPresence["Unavailable"] = "unavailable"; })(SetPresence || (SetPresence = {})); - -/** - * Internal class - unstable. - * Construct an entity which is able to sync with a homeserver. - * @constructor - * @param {MatrixClient} client The matrix client instance to use. - * @param {Object} opts Config options - * @param {module:crypto=} opts.crypto Crypto manager - * @param {Function=} opts.canResetEntireTimeline A function which is called - * with a room ID and returns a boolean. It should return 'true' if the SDK can - * SAFELY remove events from this room. It may not be safe to remove events if - * there are other references to the timelines for this room. - * Default: returns false. - * @param {Boolean=} opts.disablePresence True to perform syncing without automatically - * updating presence. - */ +/** add default settings to an IStoredClientOpts */ +function defaultClientOpts(opts) { + return _objectSpread({ + initialSyncLimit: 8, + resolveInvitesToProfiles: false, + pollTimeout: 30 * 1000, + pendingEventOrdering: _client.PendingEventOrdering.Chronological, + threadSupport: false + }, opts); +} +function defaultSyncApiOpts(syncOpts) { + return _objectSpread({ + canResetEntireTimeline: _roomId => false + }, syncOpts); +} class SyncApi { // additional data (eg. error object for failed sync) + // accumulator of sync events in the current sync response // Number of consecutive failed /sync requests // flag set if the store needs to be cleared before we can start - constructor(client, opts = {}) { - this.client = client; - this.opts = opts; + /** + * Construct an entity which is able to sync with a homeserver. + * @param client - The matrix client instance to use. + * @param opts - client config options + * @param syncOpts - sync-specific options passed by the client + * @internal + */ + constructor(client, opts, syncOpts) { + this.client = client; + _defineProperty(this, "opts", void 0); + _defineProperty(this, "syncOpts", void 0); _defineProperty(this, "_peekRoom", null); - - _defineProperty(this, "currentSyncRequest", null); - + _defineProperty(this, "currentSyncRequest", void 0); + _defineProperty(this, "abortController", void 0); _defineProperty(this, "syncState", null); - - _defineProperty(this, "syncStateData", null); - + _defineProperty(this, "syncStateData", void 0); _defineProperty(this, "catchingUp", false); - _defineProperty(this, "running", false); - - _defineProperty(this, "keepAliveTimer", null); - - _defineProperty(this, "connectionReturnedDefer", null); - + _defineProperty(this, "keepAliveTimer", void 0); + _defineProperty(this, "connectionReturnedDefer", void 0); _defineProperty(this, "notifEvents", []); - _defineProperty(this, "failedSyncCount", 0); - _defineProperty(this, "storeIsInvalid", false); - _defineProperty(this, "getPushRules", async () => { try { debuglog("Getting push rules..."); @@ -141,100 +128,84 @@ this.client.pushRules = result; } catch (err) { _logger.logger.error("Getting push rules failed", err); - - if (this.shouldAbortSync(err)) return; // wait for saved sync to complete before doing anything else, + if (this.shouldAbortSync(err)) return; + // wait for saved sync to complete before doing anything else, // otherwise the sync state will end up being incorrect - debuglog("Waiting for saved sync before retrying push rules..."); await this.recoverFromSyncStartupError(this.savedSyncPromise, err); return this.getPushRules(); // try again } }); - _defineProperty(this, "buildDefaultFilter", () => { - return new _filter.Filter(this.client.credentials.userId); + const filter = new _filter.Filter(this.client.credentials.userId); + if (this.client.canSupport.get(_feature.Feature.ThreadUnreadNotifications) !== _feature.ServerSupport.Unsupported) { + filter.setUnreadThreadNotifications(true); + } + return filter; }); - _defineProperty(this, "checkLazyLoadStatus", async () => { debuglog("Checking lazy load status..."); - if (this.opts.lazyLoadMembers && this.client.isGuest()) { this.opts.lazyLoadMembers = false; } - if (this.opts.lazyLoadMembers) { debuglog("Checking server lazy load support..."); const supported = await this.client.doesServerSupportLazyLoading(); - if (supported) { debuglog("Enabling lazy load on sync filter..."); - if (!this.opts.filter) { this.opts.filter = this.buildDefaultFilter(); } - this.opts.filter.setLazyLoadMembers(true); } else { debuglog("LL: lazy loading requested but not supported " + "by server, so disabling"); this.opts.lazyLoadMembers = false; } - } // need to vape the store when enabling LL and wasn't enabled before - - + } + // need to vape the store when enabling LL and wasn't enabled before debuglog("Checking whether lazy loading has changed in store..."); const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers); - if (shouldClear) { this.storeIsInvalid = true; - const reason = _errors.InvalidStoreError.TOGGLED_LAZY_LOADING; - const error = new _errors.InvalidStoreError(reason, !!this.opts.lazyLoadMembers); + const error = new _errors.InvalidStoreError(_errors.InvalidStoreState.ToggledLazyLoading, !!this.opts.lazyLoadMembers); this.updateSyncState(SyncState.Error, { error - }); // bail out of the sync loop now: the app needs to respond to this error. + }); + // bail out of the sync loop now: the app needs to respond to this error. // we leave the state as 'ERROR' which isn't great since this normally means // we're retrying. The client must be stopped before clearing the stores anyway // so the app should stop the client, clear the store and start it again. - _logger.logger.warn("InvalidStoreError: store is not usable: stopping sync."); - return; } - if (this.opts.lazyLoadMembers) { - this.opts.crypto?.enableLazyLoading(); + this.syncOpts.crypto?.enableLazyLoading(); } - try { debuglog("Storing client options..."); await this.client.storeClientOptions(); debuglog("Stored client options"); } catch (err) { _logger.logger.error("Storing client options failed", err); - throw err; } }); - _defineProperty(this, "getFilter", async () => { debuglog("Getting filter..."); let filter; - if (this.opts.filter) { filter = this.opts.filter; } else { filter = this.buildDefaultFilter(); } - let filterId; - try { filterId = await this.client.getOrCreateFilter(getFilterName(this.client.credentials.userId), filter); } catch (err) { _logger.logger.error("Getting filter failed", err); - - if (this.shouldAbortSync(err)) return {}; // wait for saved sync to complete before doing anything else, + if (this.shouldAbortSync(err)) return {}; + // wait for saved sync to complete before doing anything else, // otherwise the sync state will end up being incorrect - debuglog("Waiting for saved sync before retrying filter..."); await this.recoverFromSyncStartupError(this.savedSyncPromise, err); return this.getFilter(); // try again @@ -245,55 +216,34 @@ filterId }; }); - _defineProperty(this, "savedSyncPromise", void 0); - _defineProperty(this, "onOnline", () => { debuglog("Browser thinks we are back online"); this.startKeepAlives(0); }); - - this.opts.initialSyncLimit = this.opts.initialSyncLimit ?? 8; - this.opts.resolveInvitesToProfiles = this.opts.resolveInvitesToProfiles || false; - this.opts.pollTimeout = this.opts.pollTimeout || 30 * 1000; - this.opts.pendingEventOrdering = this.opts.pendingEventOrdering || _client.PendingEventOrdering.Chronological; - this.opts.experimentalThreadSupport = this.opts.experimentalThreadSupport === true; - - if (!opts.canResetEntireTimeline) { - opts.canResetEntireTimeline = roomId => { - return false; - }; - } - + this.opts = defaultClientOpts(opts); + this.syncOpts = defaultSyncApiOpts(syncOpts); if (client.getNotifTimelineSet()) { client.reEmitter.reEmit(client.getNotifTimelineSet(), [_room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset]); } } - /** - * @param {string} roomId - * @return {Room} - */ - - createRoom(roomId) { const room = _createAndReEmitRoom(this.client, roomId, this.opts); - room.on(_roomState.RoomStateEvent.Marker, (markerEvent, markerFoundOptions) => { this.onMarkerStateEvent(room, markerEvent, markerFoundOptions); }); return room; } + /** When we see the marker state change in the room, we know there is some * new historical messages imported by MSC2716 `/batch_send` somewhere in * the room and we need to throw away the timeline to make sure the * historical messages are shown when we paginate `/messages` again. - * @param {Room} room The room where the marker event was sent - * @param {MatrixEvent} markerEvent The new marker event - * @param {IMarkerFoundOptions} setStateOptions When `timelineWasEmpty` is set + * @param room - The room where the marker event was sent + * @param markerEvent - The new marker event + * @param setStateOptions - When `timelineWasEmpty` is set * as `true`, the given marker event will be ignored - */ - - + */ onMarkerStateEvent(room, markerEvent, { timelineWasEmpty } = {}) { @@ -304,17 +254,19 @@ // 3. Or whether it's coming from `syncFromCache` if (timelineWasEmpty) { _logger.logger.debug(`MarkerState: Ignoring markerEventId=${markerEvent.getId()} in roomId=${room.roomId} ` + `because the timeline was empty before the marker arrived which means there is nothing to refresh.`); - return; } - - const isValidMsc2716Event = // Check whether the room version directly supports MSC2716, in + const isValidMsc2716Event = + // Check whether the room version directly supports MSC2716, in // which case, "marker" events are already auth'ed by // power_levels - MSC2716_ROOM_VERSIONS.includes(room.getVersion()) || // MSC2716 is also supported in all existing room versions but + MSC2716_ROOM_VERSIONS.includes(room.getVersion()) || + // MSC2716 is also supported in all existing room versions but // special meaning should only be given to "insertion", "batch", // and "marker" events when they come from the room creator - markerEvent.getSender() === room.getCreator(); // It would be nice if we could also specifically tell whether the + markerEvent.getSender() === room.getCreator(); + + // It would be nice if we could also specifically tell whether the // historical messages actually affected the locally cached client // timeline or not. The problem is we can't see the prev_events of // the base insertion event that the marker was pointing to because @@ -326,89 +278,86 @@ // are likely to encounter the historical messages affecting their // current timeline (think someone signing up for Beeper and // importing their Whatsapp history). - if (isValidMsc2716Event) { // Saw new marker event, let's let the clients know they should // refresh the timeline. _logger.logger.debug(`MarkerState: Timeline needs to be refreshed because ` + `a new markerEventId=${markerEvent.getId()} was sent in roomId=${room.roomId}`); - room.setTimelineNeedsRefresh(true); room.emit(_room.RoomEvent.HistoryImportedWithinTimeline, markerEvent, room); } else { _logger.logger.debug(`MarkerState: Ignoring markerEventId=${markerEvent.getId()} in roomId=${room.roomId} because ` + `MSC2716 is not supported in the room version or for any room version, the marker wasn't sent ` + `by the room creator.`); } } + /** * Sync rooms the user has left. - * @return {Promise} Resolved when they've been added to the store. + * @returns Resolved when they've been added to the store. */ + async syncLeftRooms() { + const client = this.client; - - syncLeftRooms() { - const client = this.client; // grab a filter with limit=1 and include_leave=true - + // grab a filter with limit=1 and include_leave=true const filter = new _filter.Filter(this.client.credentials.userId); filter.setTimelineLimit(1); filter.setIncludeLeaveRooms(true); const localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS; + const filterId = await client.getOrCreateFilter(getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter); const qps = { - timeout: 0 // don't want to block since this is a single isolated req - + timeout: 0, + // don't want to block since this is a single isolated req + filter: filterId }; - return client.getOrCreateFilter(getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter).then(function (filterId) { - qps.filter = filterId; - return client.http.authedRequest(undefined, _httpApi.Method.Get, "/sync", qps, undefined, localTimeoutMs); - }).then(async data => { - let leaveRooms = []; - - if (data.rooms?.leave) { - leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave); + const data = await client.http.authedRequest(_httpApi.Method.Get, "/sync", qps, undefined, { + localTimeoutMs + }); + let leaveRooms = []; + if (data.rooms?.leave) { + leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave); + } + const rooms = await Promise.all(leaveRooms.map(async leaveObj => { + const room = leaveObj.room; + if (!leaveObj.isBrandNewRoom) { + // the intention behind syncLeftRooms is to add in rooms which were + // *omitted* from the initial /sync. Rooms the user were joined to + // but then left whilst the app is running will appear in this list + // and we do not want to bother with them since they will have the + // current state already (and may get dupe messages if we add + // yet more timeline events!), so skip them. + // NB: When we persist rooms to localStorage this will be more + // complicated... + return; } + leaveObj.timeline = leaveObj.timeline || { + prev_batch: null, + events: [] + }; + const events = this.mapSyncEventsFormat(leaveObj.timeline, room); + const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room); - return Promise.all(leaveRooms.map(async leaveObj => { - const room = leaveObj.room; - - if (!leaveObj.isBrandNewRoom) { - // the intention behind syncLeftRooms is to add in rooms which were - // *omitted* from the initial /sync. Rooms the user were joined to - // but then left whilst the app is running will appear in this list - // and we do not want to bother with them since they will have the - // current state already (and may get dupe messages if we add - // yet more timeline events!), so skip them. - // NB: When we persist rooms to localStorage this will be more - // complicated... - return; - } - - leaveObj.timeline = leaveObj.timeline || {}; - const events = this.mapSyncEventsFormat(leaveObj.timeline, room); - const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room); // set the back-pagination token. Do this *before* adding any - // events so that clients can start back-paginating. - - room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, _eventTimeline.EventTimeline.BACKWARDS); - await this.processRoomEvents(room, stateEvents, events); - room.recalculate(); - client.store.storeRoom(room); - client.emit(_client.ClientEvent.Room, room); - this.processEventsForNotifs(room, events); - return room; - })); - }); + // set the back-pagination token. Do this *before* adding any + // events so that clients can start back-paginating. + room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, _eventTimeline.EventTimeline.BACKWARDS); + await this.injectRoomEvents(room, stateEvents, events); + room.recalculate(); + client.store.storeRoom(room); + client.emit(_client.ClientEvent.Room, room); + this.processEventsForNotifs(room, events); + return room; + })); + return rooms.filter(Boolean); } + /** * Peek into a room. This will result in the room in question being synced so it * is accessible via getRooms(). Live updates for the room will be provided. - * @param {string} roomId The room ID to peek into. - * @return {Promise} A promise which resolves once the room has been added to the + * @param roomId - The room ID to peek into. + * @returns A promise which resolves once the room has been added to the * store. */ - - peek(roomId) { - if (this._peekRoom && this._peekRoom.roomId === roomId) { + if (this._peekRoom?.roomId === roomId) { return Promise.resolve(this._peekRoom); } - const client = this.client; this._peekRoom = this.createRoom(roomId); return this.client.roomInitialSync(roomId, 20).then(response => { @@ -417,18 +366,19 @@ chunk: [] }; response.messages.chunk = response.messages.chunk || []; - response.state = response.state || []; // FIXME: Mostly duplicated from processRoomEvents but not entirely - // because "state" in this API is at the BEGINNING of the chunk + response.state = response.state || []; + // FIXME: Mostly duplicated from injectRoomEvents but not entirely + // because "state" in this API is at the BEGINNING of the chunk const oldStateEvents = utils.deepCopy(response.state).map(client.getEventMapper()); const stateEvents = response.state.map(client.getEventMapper()); - const messages = response.messages.chunk.map(client.getEventMapper()); // XXX: copypasted from /sync until we kill off this minging v1 API stuff) - // handle presence events (User objects) + const messages = response.messages.chunk.map(client.getEventMapper()); + // XXX: copypasted from /sync until we kill off this minging v1 API stuff) + // handle presence events (User objects) if (Array.isArray(response.presence)) { response.presence.map(client.getEventMapper()).forEach(function (presenceEvent) { let user = client.store.getUser(presenceEvent.getContent().user_id); - if (user) { user.setPresenceEvent(presenceEvent); } else { @@ -436,70 +386,66 @@ user.setPresenceEvent(presenceEvent); client.store.storeUser(user); } - client.emit(_client.ClientEvent.Event, presenceEvent); }); - } // set the pagination token before adding the events in case people + } + + // set the pagination token before adding the events in case people // fire off pagination requests in response to the Room.timeline // events. - - if (response.messages.start) { this._peekRoom.oldState.paginationToken = response.messages.start; - } // set the state of the room to as it was after the timeline executes - + } + // set the state of the room to as it was after the timeline executes this._peekRoom.oldState.setStateEvents(oldStateEvents); - this._peekRoom.currentState.setStateEvents(stateEvents); - this.resolveInvites(this._peekRoom); + this._peekRoom.recalculate(); - this._peekRoom.recalculate(); // roll backwards to diverge old state. addEventsToTimeline + // roll backwards to diverge old state. addEventsToTimeline // will overwrite the pagination token, so make sure it overwrites // it with the right thing. - - this._peekRoom.addEventsToTimeline(messages.reverse(), true, this._peekRoom.getLiveTimeline(), response.messages.start); - client.store.storeRoom(this._peekRoom); client.emit(_client.ClientEvent.Room, this._peekRoom); this.peekPoll(this._peekRoom); return this._peekRoom; }); } + /** * Stop polling for updates in the peeked room. NOPs if there is no room being * peeked. */ - - stopPeeking() { this._peekRoom = null; } + /** * Do a peek room poll. - * @param {Room} peekRoom - * @param {string?} token from= token + * @param token - from= token */ - - peekPoll(peekRoom, token) { if (this._peekRoom !== peekRoom) { debuglog("Stopped peeking in room %s", peekRoom.roomId); return; - } // FIXME: gut wrenching; hard-coded timeout values - + } - this.client.http.authedRequest(undefined, _httpApi.Method.Get, "/events", { + // FIXME: gut wrenching; hard-coded timeout values + this.client.http.authedRequest(_httpApi.Method.Get, "/events", { room_id: peekRoom.roomId, timeout: String(30 * 1000), from: token - }, undefined, 50 * 1000).then(res => { + }, undefined, { + localTimeoutMs: 50 * 1000, + abortSignal: this.abortController?.signal + }).then(res => { if (this._peekRoom !== peekRoom) { debuglog("Stopped peeking in room %s", peekRoom.roomId); return; - } // We have a problem that we get presence both from /events and /sync + } + // We have a problem that we get presence both from /events and /sync // however, /sync only returns presence for users in rooms // you're actually joined to. // in order to be sure to get presence for all of the users in the @@ -508,12 +454,10 @@ // performance drain, but such is life. // XXX: copypasted from /sync until we can kill this minging v1 stuff. - res.chunk.filter(function (e) { return e.type === "m.presence"; }).map(this.client.getEventMapper()).forEach(presenceEvent => { let user = this.client.store.getUser(presenceEvent.getContent().user_id); - if (user) { user.setPresenceEvent(presenceEvent); } else { @@ -521,12 +465,12 @@ user.setPresenceEvent(presenceEvent); this.client.store.storeUser(user); } - this.client.emit(_client.ClientEvent.Event, presenceEvent); - }); // strip out events which aren't for the given room_id (e.g presence) + }); + + // strip out events which aren't for the given room_id (e.g presence) // and also ephemeral events (which we're assuming is anything without // and event ID because the /events API doesn't separate them). - const events = res.chunk.filter(function (e) { return e.room_id === peekRoom.roomId && e.event_id; }).map(this.client.getEventMapper()); @@ -534,37 +478,31 @@ this.peekPoll(peekRoom, res.end); }, err => { _logger.logger.error("[%s] Peek poll failed: %s", peekRoom.roomId, err); - setTimeout(() => { this.peekPoll(peekRoom, token); }, 30 * 1000); }); } + /** * Returns the current state of this sync object - * @see module:client~MatrixClient#event:"sync" - * @return {?String} + * @see MatrixClient#event:"sync" */ - - getSyncState() { return this.syncState; } + /** * Returns the additional data object associated with * the current sync state, or null if there is no * such data. * Sync errors, if available, are put in the 'error' key of * this object. - * @return {?Object} */ - - getSyncStateData() { - return this.syncStateData; + return this.syncStateData ?? null; } - - async recoverFromSyncStartupError(savedSyncPromise, err) { + async recoverFromSyncStartupError(savedSyncPromise, error) { // Wait for the saved sync to complete - we send the pushrules and filter requests // before the saved sync has finished so they can run in parallel, but only process // the results after the saved sync is done. Equivalently, we wait for it to finish @@ -572,65 +510,56 @@ await savedSyncPromise; const keepaliveProm = this.startKeepAlives(); this.updateSyncState(SyncState.Error, { - error: err + error }); await keepaliveProm; } + /** * Is the lazy loading option different than in previous session? - * @param {boolean} lazyLoadMembers current options for lazy loading - * @return {boolean} whether or not the option has changed compared to the previous session */ - - + * @param lazyLoadMembers - current options for lazy loading + * @returns whether or not the option has changed compared to the previous session */ async wasLazyLoadingToggled(lazyLoadMembers = false) { // assume it was turned off before // if we don't know any better let lazyLoadMembersBefore = false; const isStoreNewlyCreated = await this.client.store.isNewlyCreated(); - if (!isStoreNewlyCreated) { const prevClientOptions = await this.client.store.getClientOptions(); - if (prevClientOptions) { lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers; } - return lazyLoadMembersBefore !== lazyLoadMembers; } - return false; } - shouldAbortSync(error) { if (error.errcode === "M_UNKNOWN_TOKEN") { // The logout already happened, we just need to stop. _logger.logger.warn("Token no longer valid - assuming logout"); - this.stop(); this.updateSyncState(SyncState.Error, { error }); return true; } - return false; } - /** * Main entry point */ async sync() { this.running = true; + this.abortController = new AbortController(); global.window?.addEventListener?.("online", this.onOnline, false); - if (this.client.isGuest()) { // no push rules for guests, no access to POST filter for guests. return this.doSync({}); - } // Pull the saved sync token out first, before the worker starts sending + } + + // Pull the saved sync token out first, before the worker starts sending // all the sync data which could take a while. This will let us send our // first incremental sync request before we've processed our saved data. - - debuglog("Getting saved sync token..."); const savedSyncTokenPromise = this.client.store.getSavedSyncToken().then(tok => { debuglog("Got saved sync token"); @@ -638,23 +567,24 @@ }); this.savedSyncPromise = this.client.store.getSavedSync().then(savedSync => { debuglog(`Got reply from saved sync, exists? ${!!savedSync}`); - if (savedSync) { return this.syncFromCache(savedSync); } }).catch(err => { _logger.logger.error("Getting saved sync failed", err); - }); // We need to do one-off checks before we can begin the /sync loop. + }); + + // We need to do one-off checks before we can begin the /sync loop. // These are: // 1) We need to get push rules so we can check if events should bing as we get // them from /sync. // 2) We need to get/create a filter which we can use for /sync. // 3) We need to check the lazy loading option matches what was used in the // stored sync. If it doesn't, we can't use the stored sync. + // Now start the first incremental sync request: this can also // take a while so if we set it going now, we can wait for it // to finish while we process our saved sync data. - await this.getPushRules(); await this.checkLazyLoadStatus(); const { @@ -662,91 +592,85 @@ filter } = await this.getFilter(); if (!filter) return; // bail, getFilter failed + // reset the notifications timeline to prepare it to paginate from // the current point in time. // The right solution would be to tie /sync pagination tokens into // /notifications API somehow. - this.client.resetNotifTimelineSet(); - - if (this.currentSyncRequest === null) { + if (!this.currentSyncRequest) { let firstSyncFilter = filterId; const savedSyncToken = await savedSyncTokenPromise; - if (savedSyncToken) { debuglog("Sending first sync request..."); } else { debuglog("Sending initial sync request..."); const initialFilter = this.buildDefaultFilter(); initialFilter.setDefinition(filter.getDefinition()); - initialFilter.setTimelineLimit(this.opts.initialSyncLimit); // Use an inline filter, no point uploading it for a single usage - + initialFilter.setTimelineLimit(this.opts.initialSyncLimit); + // Use an inline filter, no point uploading it for a single usage firstSyncFilter = JSON.stringify(initialFilter.getDefinition()); - } // Send this first sync request here so we can then wait for the saved - // sync data to finish processing before we process the results of this one. - + } + // Send this first sync request here so we can then wait for the saved + // sync data to finish processing before we process the results of this one. this.currentSyncRequest = this.doSyncRequest({ filter: firstSyncFilter }, savedSyncToken); - } // Now wait for the saved sync to finish... - + } + // Now wait for the saved sync to finish... debuglog("Waiting for saved sync before starting sync processing..."); - await this.savedSyncPromise; // process the first sync request and continue syncing with the normal filterId - + await this.savedSyncPromise; + // process the first sync request and continue syncing with the normal filterId return this.doSync({ filter: filterId }); } + /** * Stops the sync object from syncing. */ - - stop() { - debuglog("SyncApi.stop"); // It is necessary to check for the existance of + debuglog("SyncApi.stop"); + // It is necessary to check for the existance of // global.window AND global.window.removeEventListener. // Some platforms (e.g. React Native) register global.window, // but do not have global.window.removeEventListener. - global.window?.removeEventListener?.("online", this.onOnline, false); this.running = false; - this.currentSyncRequest?.abort(); - + this.abortController?.abort(); if (this.keepAliveTimer) { clearTimeout(this.keepAliveTimer); - this.keepAliveTimer = null; + this.keepAliveTimer = undefined; } } + /** * Retry a backed off syncing request immediately. This should only be used when * the user explicitly attempts to retry their lost connection. - * @return {boolean} True if this resulted in a request being retried. + * @returns True if this resulted in a request being retried. */ - - retryImmediately() { if (!this.connectionReturnedDefer) { return false; } - this.startKeepAlives(0); return true; } /** * Process a single set of cached sync data. - * @param {Object} savedSync a saved sync that was persisted by a store. This + * @param savedSync - a saved sync that was persisted by a store. This * should have been acquired via client.store.getSavedSync(). */ - - async syncFromCache(savedSync) { debuglog("sync(): not doing HTTP hit, instead returning stored /sync data"); - const nextSyncToken = savedSync.nextBatch; // Set sync token for future incremental syncing + const nextSyncToken = savedSync.nextBatch; - this.client.store.setSyncToken(nextSyncToken); // No previous sync, set old token to null + // Set sync token for future incremental syncing + this.client.store.setSyncToken(nextSyncToken); + // No previous sync, set old token to null const syncEventData = { nextSyncToken, catchingUp: false, @@ -759,131 +683,118 @@ events: savedSync.accountData } }; - try { await this.processSyncResponse(syncEventData, data); } catch (e) { _logger.logger.error("Error processing cached sync", e); - } // Don't emit a prepared if we've bailed because the store is invalid: + } + + // Don't emit a prepared if we've bailed because the store is invalid: // in this case the client will not be usable until stopped & restarted // so this would be useless and misleading. - - if (!this.storeIsInvalid) { this.updateSyncState(SyncState.Prepared, syncEventData); } } + /** * Invoke me to do /sync calls - * @param {Object} syncOptions - * @param {string} syncOptions.filterId - * @param {boolean} syncOptions.hasSyncedBefore */ - - async doSync(syncOptions) { while (this.running) { const syncToken = this.client.store.getSyncToken(); let data; - try { - //debuglog('Starting sync since=' + syncToken); - if (this.currentSyncRequest === null) { + if (!this.currentSyncRequest) { this.currentSyncRequest = this.doSyncRequest(syncOptions, syncToken); } - data = await this.currentSyncRequest; } catch (e) { const abort = await this.onSyncError(e); if (abort) return; continue; } finally { - this.currentSyncRequest = null; - } //debuglog('Completed sync, next_batch=' + data.next_batch); + this.currentSyncRequest = undefined; + } + // set the sync token NOW *before* processing the events. We do this so // if something barfs on an event we can skip it rather than constantly // polling with the same token. + this.client.store.setSyncToken(data.next_batch); - - this.client.store.setSyncToken(data.next_batch); // Reset after a successful sync - + // Reset after a successful sync this.failedSyncCount = 0; await this.client.store.setSyncData(data); const syncEventData = { - oldSyncToken: syncToken, + oldSyncToken: syncToken ?? undefined, nextSyncToken: data.next_batch, catchingUp: this.catchingUp }; - - if (this.opts.crypto) { + if (this.syncOpts.crypto) { // tell the crypto module we're about to process a sync // response - await this.opts.crypto.onSyncWillProcess(syncEventData); + await this.syncOpts.crypto.onSyncWillProcess(syncEventData); } - try { await this.processSyncResponse(syncEventData, data); } catch (e) { // log the exception with stack if we have it, else fall back // to the plain description - _logger.logger.error("Caught /sync error", e); // Emit the exception for client handling - + _logger.logger.error("Caught /sync error", e); + // Emit the exception for client handling this.client.emit(_client.ClientEvent.SyncUnexpectedError, e); - } // update this as it may have changed - + } - syncEventData.catchingUp = this.catchingUp; // emit synced events + // update this as it may have changed + syncEventData.catchingUp = this.catchingUp; + // emit synced events if (!syncOptions.hasSyncedBefore) { this.updateSyncState(SyncState.Prepared, syncEventData); syncOptions.hasSyncedBefore = true; - } // tell the crypto module to do its processing. It may block (to do a - // /keys/changes request). - - - if (this.opts.crypto) { - await this.opts.crypto.onSyncCompleted(syncEventData); - } // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates + } + // tell the crypto module to do its processing. It may block (to do a + // /keys/changes request). + if (this.syncOpts.cryptoCallbacks) { + await this.syncOpts.cryptoCallbacks.onSyncCompleted(syncEventData); + } + // keep emitting SYNCING -> SYNCING for clients who want to do bulk updates this.updateSyncState(SyncState.Syncing, syncEventData); - if (this.client.store.wantsSave()) { // We always save the device list (if it's dirty) before saving the sync data: // this means we know the saved device list data is at least as fresh as the // stored sync data which means we don't have to worry that we may have missed // device changes. We can also skip the delay since we're not calling this very // frequently (and we don't really want to delay the sync for it). - if (this.opts.crypto) { - await this.opts.crypto.saveDeviceList(0); - } // tell databases that everything is now in a consistent state and can be saved. - + if (this.syncOpts.crypto) { + await this.syncOpts.crypto.saveDeviceList(0); + } + // tell databases that everything is now in a consistent state and can be saved. this.client.store.save(); } } - if (!this.running) { debuglog("Sync no longer running: exiting."); - if (this.connectionReturnedDefer) { this.connectionReturnedDefer.reject(); - this.connectionReturnedDefer = null; + this.connectionReturnedDefer = undefined; } - this.updateSyncState(SyncState.Stopped); } } - doSyncRequest(syncOptions, syncToken) { const qps = this.getSyncParams(syncOptions, syncToken); - return this.client.http.authedRequest(undefined, _httpApi.Method.Get, "/sync", qps, undefined, qps.timeout + BUFFER_PERIOD_MS); + return this.client.http.authedRequest(_httpApi.Method.Get, "/sync", qps, undefined, { + localTimeoutMs: qps.timeout + BUFFER_PERIOD_MS, + abortSignal: this.abortController?.signal + }); } - getSyncParams(syncOptions, syncToken) { - let pollTimeout = this.opts.pollTimeout; - + let timeout = this.opts.pollTimeout; if (this.getSyncState() !== SyncState.Syncing || this.catchingUp) { // unless we are happily syncing already, we want the server to return // as quickly as possible, even if there are no events queued. This @@ -897,24 +808,19 @@ // for us. We do that by calling it with a zero timeout until it // doesn't give us any more to_device messages. this.catchingUp = true; - pollTimeout = 0; + timeout = 0; } - let filter = syncOptions.filter; - if (this.client.isGuest() && !filter) { filter = this.getGuestFilter(); } - const qps = { filter, - timeout: pollTimeout + timeout }; - if (this.opts.disablePresence) { qps.set_presence = SetPresence.Offline; } - if (syncToken) { qps.since = syncToken; } else { @@ -923,7 +829,6 @@ // (https://github.com/vector-im/vector-web/issues/1354) qps._cacheBuster = Date.now(); } - if ([SyncState.Reconnecting, SyncState.Error].includes(this.getSyncState())) { // we think the connection is dead. If it comes back up, we won't know // about it till /sync returns. If the timeout= is high, this could @@ -931,71 +836,65 @@ // for an event or a timeout before emiting the SYNCING event. qps.timeout = 0; } - return qps; } - async onSyncError(err) { if (!this.running) { debuglog("Sync no longer running: exiting"); - if (this.connectionReturnedDefer) { this.connectionReturnedDefer.reject(); - this.connectionReturnedDefer = null; + this.connectionReturnedDefer = undefined; } - this.updateSyncState(SyncState.Stopped); return true; // abort } _logger.logger.error("/sync error %s", err); - if (this.shouldAbortSync(err)) { return true; // abort } this.failedSyncCount++; - - _logger.logger.log('Number of consecutive failed sync requests:', this.failedSyncCount); - - debuglog("Starting keep-alive"); // Note that we do *not* mark the sync connection as + _logger.logger.log("Number of consecutive failed sync requests:", this.failedSyncCount); + debuglog("Starting keep-alive"); + // Note that we do *not* mark the sync connection as // lost yet: we only do this if a keepalive poke // fails, since long lived HTTP connections will // go away sometimes and we shouldn't treat this as // erroneous. We set the state to 'reconnecting' // instead, so that clients can observe this state // if they wish. - const keepAlivePromise = this.startKeepAlives(); - this.currentSyncRequest = null; // Transition from RECONNECTING to ERROR after a given number of failed syncs - + this.currentSyncRequest = undefined; + // Transition from RECONNECTING to ERROR after a given number of failed syncs this.updateSyncState(this.failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? SyncState.Error : SyncState.Reconnecting, { error: err }); - const connDidFail = await keepAlivePromise; // Only emit CATCHUP if we detected a connectivity error: if we didn't, + const connDidFail = await keepAlivePromise; + + // Only emit CATCHUP if we detected a connectivity error: if we didn't, // it's quite likely the sync will fail again for the same reason and we // want to stay in ERROR rather than keep flip-flopping between ERROR // and CATCHUP. - if (connDidFail && this.getSyncState() === SyncState.Error) { this.updateSyncState(SyncState.Catchup, { catchingUp: true }); } - return false; } + /** * Process data returned from a sync response and propagate it * into the model objects * - * @param {Object} syncEventData Object containing sync tokens associated with this sync - * @param {Object} data The response from /sync + * @param syncEventData - Object containing sync tokens associated with this sync + * @param data - The response from /sync */ - - async processSyncResponse(syncEventData, data) { - const client = this.client; // data looks like: + const client = this.client; + + // data looks like: // { // next_batch: $token, // presence: { events: [] }, @@ -1034,16 +933,16 @@ // } // } // } + // TODO-arch: // - Each event we pass through needs to be emitted via 'event', can we // do this in one place? // - The isBrandNewRoom boilerplate is boilerplatey. - // handle presence events (User objects) + // handle presence events (User objects) if (Array.isArray(data.presence?.events)) { - data.presence.events.map(client.getEventMapper()).forEach(function (presenceEvent) { + data.presence.events.filter(utils.noUnsafeEventProps).map(client.getEventMapper()).forEach(function (presenceEvent) { let user = client.store.getUser(presenceEvent.getSender()); - if (user) { user.setPresenceEvent(presenceEvent); } else { @@ -1051,16 +950,15 @@ user.setPresenceEvent(presenceEvent); client.store.storeUser(user); } - client.emit(_client.ClientEvent.Event, presenceEvent); }); - } // handle non-room account_data - + } + // handle non-room account_data if (Array.isArray(data.account_data?.events)) { - const events = data.account_data.events.map(client.getEventMapper()); + const events = data.account_data.events.filter(utils.noUnsafeEventProps).map(client.getEventMapper()); const prevEventsMap = events.reduce((m, c) => { - m[c.getId()] = client.store.getAccountData(c.getType()); + m[c.getType()] = client.store.getAccountData(c.getType()); return m; }, {}); client.store.storeAccountDataEvents(events); @@ -1071,27 +969,22 @@ // (see sync) before syncing over the network. if (accountDataEvent.getType() === _event.EventType.PushRules) { const rules = accountDataEvent.getContent(); - client.pushRules = _pushprocessor.PushProcessor.rewriteDefaultRules(rules); + client.setPushRules(rules); } - - const prevEvent = prevEventsMap[accountDataEvent.getId()]; + const prevEvent = prevEventsMap[accountDataEvent.getType()]; client.emit(_client.ClientEvent.AccountData, accountDataEvent, prevEvent); return accountDataEvent; }); - } // handle to-device events - + } - if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) { + // handle to-device events + if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0) { + let toDeviceMessages = data.to_device.events.filter(utils.noUnsafeEventProps); + if (this.syncOpts.cryptoCallbacks) { + toDeviceMessages = await this.syncOpts.cryptoCallbacks.preprocessToDeviceMessages(toDeviceMessages); + } const cancelledKeyVerificationTxns = []; - data.to_device.events.filter(eventJSON => { - if (eventJSON.type === _event.EventType.RoomMessageEncrypted && !["m.olm.v1.curve25519-aes-sha2"].includes(eventJSON.content?.algorithm)) { - _logger.logger.log('Ignoring invalid encrypted to-device event from ' + eventJSON.sender); - - return false; - } - - return true; - }).map(client.getEventMapper({ + toDeviceMessages.map(client.getEventMapper({ toDevice: true })).map(toDeviceEvent => { // map is a cheap inline forEach @@ -1101,80 +994,72 @@ // so we can flag the verification events as cancelled in the loop // below. if (toDeviceEvent.getType() === "m.key.verification.cancel") { - const txnId = toDeviceEvent.getContent()['transaction_id']; - + const txnId = toDeviceEvent.getContent()["transaction_id"]; if (txnId) { cancelledKeyVerificationTxns.push(txnId); } - } // as mentioned above, .map is a cheap inline forEach, so return - // the unmodified event. - + } + // as mentioned above, .map is a cheap inline forEach, so return + // the unmodified event. return toDeviceEvent; }).forEach(function (toDeviceEvent) { const content = toDeviceEvent.getContent(); - if (toDeviceEvent.getType() == "m.room.message" && content.msgtype == "m.bad.encrypted") { // the mapper already logged a warning. - _logger.logger.log('Ignoring undecryptable to-device event from ' + toDeviceEvent.getSender()); - + _logger.logger.log("Ignoring undecryptable to-device event from " + toDeviceEvent.getSender()); return; } - if (toDeviceEvent.getType() === "m.key.verification.start" || toDeviceEvent.getType() === "m.key.verification.request") { - const txnId = content['transaction_id']; - + const txnId = content["transaction_id"]; if (cancelledKeyVerificationTxns.includes(txnId)) { toDeviceEvent.flagCancelled(); } } - client.emit(_client.ClientEvent.ToDeviceEvent, toDeviceEvent); }); } else { // no more to-device events: we can stop polling with a short timeout. this.catchingUp = false; - } // the returned json structure is a bit crap, so make it into a + } + + // the returned json structure is a bit crap, so make it into a // nicer form (array) after applying sanity to make sure we don't fail // on missing keys (on the off chance) - - let inviteRooms = []; let joinRooms = []; let leaveRooms = []; - if (data.rooms) { if (data.rooms.invite) { inviteRooms = this.mapSyncResponseToRoomArray(data.rooms.invite); } - if (data.rooms.join) { joinRooms = this.mapSyncResponseToRoomArray(data.rooms.join); } - if (data.rooms.leave) { leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave); } } + this.notifEvents = []; - this.notifEvents = []; // Handle invites - + // Handle invites await utils.promiseMapSeries(inviteRooms, async inviteObj => { const room = inviteObj.room; const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room); - await this.processRoomEvents(room, stateEvents); + await this.injectRoomEvents(room, stateEvents); const inviter = room.currentState.getStateEvents(_event.EventType.RoomMember, client.getUserId())?.getSender(); - const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId); - - for (const parked of parkedHistory) { - if (parked.senderId === inviter) { - await this.client.crypto.olmDevice.addInboundGroupSession(room.roomId, parked.senderKey, parked.forwardingCurve25519KeyChain, parked.sessionId, parked.sessionKey, parked.keysClaimed, true, { - sharedHistory: true, - untrusted: true - }); + const crypto = client.crypto; + if (crypto) { + const parkedHistory = await crypto.cryptoStore.takeParkedSharedHistory(room.roomId); + for (const parked of parkedHistory) { + if (parked.senderId === inviter) { + await crypto.olmDevice.addInboundGroupSession(room.roomId, parked.senderKey, parked.forwardingCurve25519KeyChain, parked.sessionId, parked.sessionKey, parked.keysClaimed, true, { + sharedHistory: true, + untrusted: true + }); + } } } - if (inviteObj.isBrandNewRoom) { room.recalculate(); client.store.storeRoom(room); @@ -1183,43 +1068,75 @@ // Update room state for invite->reject->invite cycles room.recalculate(); } - stateEvents.forEach(function (e) { client.emit(_client.ClientEvent.Event, e); }); - }); // Handle joins + }); + // Handle joins await utils.promiseMapSeries(joinRooms, async joinObj => { const room = joinObj.room; - const stateEvents = this.mapSyncEventsFormat(joinObj.state, room); // Prevent events from being decrypted ahead of time + const stateEvents = this.mapSyncEventsFormat(joinObj.state, room); + // Prevent events from being decrypted ahead of time // this helps large account to speed up faster // room::decryptCriticalEvent is in charge of decrypting all the events // required for a client to function properly - const events = this.mapSyncEventsFormat(joinObj.timeline, room, false); const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral); const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data); - const encrypted = client.isRoomEncrypted(room.roomId); // we do this first so it's correct when any of the events fire - + const encrypted = client.isRoomEncrypted(room.roomId); + // We store the server-provided value first so it's correct when any of the events fire. if (joinObj.unread_notifications) { - room.setUnreadNotificationCount(_room.NotificationCountType.Total, joinObj.unread_notifications.notification_count); // We track unread notifications ourselves in encrypted rooms, so don't - // bother setting it here. We trust our calculations better than the - // server's for this case, and therefore will assume that our non-zero - // count is accurate. - + /** + * We track unread notifications ourselves in encrypted rooms, so don't + * bother setting it here. We trust our calculations better than the + * server's for this case, and therefore will assume that our non-zero + * count is accurate. + * + * @see import("./client").fixNotificationCountOnDecryption + */ + if (!encrypted || joinObj.unread_notifications.notification_count === 0) { + // In an encrypted room, if the room has notifications enabled then it's typical for + // the server to flag all new messages as notifying. However, some push rules calculate + // events as ignored based on their event contents (e.g. ignoring msgtype=m.notice messages) + // so we want to calculate this figure on the client in all cases. + room.setUnreadNotificationCount(_room.NotificationCountType.Total, joinObj.unread_notifications.notification_count ?? 0); + } if (!encrypted || room.getUnreadNotificationCount(_room.NotificationCountType.Highlight) <= 0) { - room.setUnreadNotificationCount(_room.NotificationCountType.Highlight, joinObj.unread_notifications.highlight_count); + // If the locally stored highlight count is zero, use the server provided value. + room.setUnreadNotificationCount(_room.NotificationCountType.Highlight, joinObj.unread_notifications.highlight_count ?? 0); } } - + const unreadThreadNotifications = joinObj[_sync.UNREAD_THREAD_NOTIFICATIONS.name] ?? joinObj[_sync.UNREAD_THREAD_NOTIFICATIONS.altName]; + if (unreadThreadNotifications) { + // Only partially reset unread notification + // We want to keep the client-generated count. Particularly important + // for encrypted room that refresh their notification count on event + // decryption + room.resetThreadUnreadNotificationCount(Object.keys(unreadThreadNotifications)); + for (const [threadId, unreadNotification] of Object.entries(unreadThreadNotifications)) { + if (!encrypted || unreadNotification.notification_count === 0) { + room.setThreadUnreadNotificationCount(threadId, _room.NotificationCountType.Total, unreadNotification.notification_count ?? 0); + } + const hasNoNotifications = room.getThreadUnreadNotificationCount(threadId, _room.NotificationCountType.Highlight) <= 0; + if (!encrypted || encrypted && hasNoNotifications) { + room.setThreadUnreadNotificationCount(threadId, _room.NotificationCountType.Highlight, unreadNotification.highlight_count ?? 0); + } + } + } else { + room.resetThreadUnreadNotificationCount(); + } joinObj.timeline = joinObj.timeline || {}; - if (joinObj.isBrandNewRoom) { // set the back-pagination token. Do this *before* adding any // events so that clients can start back-paginating. - room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, _eventTimeline.EventTimeline.BACKWARDS); + if (joinObj.timeline.prev_batch !== null) { + room.getLiveTimeline().setPaginationToken(joinObj.timeline.prev_batch, _eventTimeline.EventTimeline.BACKWARDS); + } } else if (joinObj.timeline.limited) { - let limited = true; // we've got a limited sync, so we *probably* have a gap in the + let limited = true; + + // we've got a limited sync, so we *probably* have a gap in the // timeline, so should reset. But we might have been peeking or // paginating and already have some of the events, in which // case we just want to append any subsequent events to the end @@ -1231,17 +1148,18 @@ // which we'll try to paginate but not get any new events (which // will stop us linking the empty timeline into the chain). // - for (let i = events.length - 1; i >= 0; i--) { const eventId = events[i].getId(); - if (room.getTimelineForEvent(eventId)) { - debuglog("Already have event " + eventId + " in limited " + "sync - not resetting"); - limited = false; // we might still be missing some of the events before i; + debuglog(`Already have event ${eventId} in limited sync - not resetting`); + limited = false; + + // we might still be missing some of the events before i; // we don't want to be adding them to the end of the // timeline because that would put them out of order. + events.splice(0, i); - events.splice(0, i); // XXX: there's a problem here if the skipped part of the + // XXX: there's a problem here if the skipped part of the // timeline modifies the state set in stateEvents, because // we'll end up using the state from stateEvents rather // than the later state from timelineEvents. We probably @@ -1251,78 +1169,76 @@ break; } } - if (limited) { - room.resetLiveTimeline(joinObj.timeline.prev_batch, this.opts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken); // We have to assume any gap in any timeline is + room.resetLiveTimeline(joinObj.timeline.prev_batch, this.syncOpts.canResetEntireTimeline(room.roomId) ? null : syncEventData.oldSyncToken ?? null); + + // We have to assume any gap in any timeline is // reason to stop incrementally tracking notifications and // reset the timeline. - client.resetNotifTimelineSet(); } } + // process any crypto events *before* emitting the RoomStateEvent events. This + // avoids a race condition if the application tries to send a message after the + // state event is processed, but before crypto is enabled, which then causes the + // crypto layer to complain. + if (this.syncOpts.cryptoCallbacks) { + for (const e of stateEvents.concat(events)) { + if (e.isState() && e.getType() === _event.EventType.RoomEncryption && e.getStateKey() === "") { + await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e); + } + } + } try { - await this.processRoomEvents(room, stateEvents, events, syncEventData.fromCache); + await this.injectRoomEvents(room, stateEvents, events, syncEventData.fromCache); } catch (e) { _logger.logger.error(`Failed to process events on room ${room.roomId}:`, e); - } // set summary after processing events, + } + + // set summary after processing events, // because it will trigger a name calculation // which needs the room state to be up to date - - if (joinObj.summary) { room.setSummary(joinObj.summary); - } // we deliberately don't add ephemeral events to the timeline - + } - room.addEphemeralEvents(ephemeralEvents); // we deliberately don't add accountData to the timeline + // we deliberately don't add ephemeral events to the timeline + room.addEphemeralEvents(ephemeralEvents); + // we deliberately don't add accountData to the timeline room.addAccountData(accountDataEvents); room.recalculate(); - if (joinObj.isBrandNewRoom) { client.store.storeRoom(room); client.emit(_client.ClientEvent.Room, room); } - this.processEventsForNotifs(room, events); + const emitEvent = e => client.emit(_client.ClientEvent.Event, e); + stateEvents.forEach(emitEvent); + events.forEach(emitEvent); + ephemeralEvents.forEach(emitEvent); + accountDataEvents.forEach(emitEvent); - const processRoomEvent = async e => { - client.emit(_client.ClientEvent.Event, e); - - if (e.isState() && e.getType() == "m.room.encryption" && this.opts.crypto) { - await this.opts.crypto.onCryptoEvent(e); - } - }; - - await utils.promiseMapSeries(stateEvents, processRoomEvent); - await utils.promiseMapSeries(events, processRoomEvent); - ephemeralEvents.forEach(function (e) { - client.emit(_client.ClientEvent.Event, e); - }); - accountDataEvents.forEach(function (e) { - client.emit(_client.ClientEvent.Event, e); - }); // Decrypt only the last message in all rooms to make sure we can generate a preview + // Decrypt only the last message in all rooms to make sure we can generate a preview // And decrypt all events after the recorded read receipt to ensure an accurate // notification count - room.decryptCriticalEvents(); - }); // Handle leaves (e.g. kicked rooms) + }); + // Handle leaves (e.g. kicked rooms) await utils.promiseMapSeries(leaveRooms, async leaveObj => { const room = leaveObj.room; const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room); const events = this.mapSyncEventsFormat(leaveObj.timeline, room); const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data); - await this.processRoomEvents(room, stateEvents, events); + await this.injectRoomEvents(room, stateEvents, events); room.addAccountData(accountDataEvents); room.recalculate(); - if (leaveObj.isBrandNewRoom) { client.store.storeRoom(room); client.emit(_client.ClientEvent.Room, room); } - this.processEventsForNotifs(room, events); stateEvents.forEach(function (e) { client.emit(_client.ClientEvent.Event, e); @@ -1333,75 +1249,72 @@ accountDataEvents.forEach(function (e) { client.emit(_client.ClientEvent.Event, e); }); - }); // update the notification timeline, if appropriate. + }); + + // update the notification timeline, if appropriate. // we only do this for live events, as otherwise we can't order them sanely // in the timeline relative to ones paginated in by /notifications. // XXX: we could fix this by making EventTimeline support chronological // ordering... but it doesn't, right now. - if (syncEventData.oldSyncToken && this.notifEvents.length) { this.notifEvents.sort(function (a, b) { return a.getTs() - b.getTs(); }); this.notifEvents.forEach(function (event) { - client.getNotifTimelineSet().addLiveEvent(event); + client.getNotifTimelineSet()?.addLiveEvent(event); }); - } // Handle device list updates - + } + // Handle device list updates if (data.device_lists) { - if (this.opts.crypto) { - await this.opts.crypto.handleDeviceListChanges(syncEventData, data.device_lists); - } else {// FIXME if we *don't* have a crypto module, we still need to + if (this.syncOpts.crypto) { + await this.syncOpts.crypto.handleDeviceListChanges(syncEventData, data.device_lists); + } else { + // FIXME if we *don't* have a crypto module, we still need to // invalidate the device lists. But that would require a // substantial bit of rework :/. } - } // Handle one_time_keys_count - + } - if (this.opts.crypto && data.device_one_time_keys_count) { + // Handle one_time_keys_count + if (this.syncOpts.crypto && data.device_one_time_keys_count) { const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0; - this.opts.crypto.updateOneTimeKeyCount(currentCount); + this.syncOpts.crypto.updateOneTimeKeyCount(currentCount); } - - if (this.opts.crypto && (data["device_unused_fallback_key_types"] || data["org.matrix.msc2732.device_unused_fallback_key_types"])) { + if (this.syncOpts.crypto && (data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"])) { // The presence of device_unused_fallback_key_types indicates that the // server supports fallback keys. If there's no unused // signed_curve25519 fallback key we need a new one. - const unusedFallbackKeys = data["device_unused_fallback_key_types"] || data["org.matrix.msc2732.device_unused_fallback_key_types"]; - this.opts.crypto.setNeedsNewFallback(unusedFallbackKeys instanceof Array && !unusedFallbackKeys.includes("signed_curve25519")); + const unusedFallbackKeys = data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"]; + this.syncOpts.crypto.setNeedsNewFallback(Array.isArray(unusedFallbackKeys) && !unusedFallbackKeys.includes("signed_curve25519")); } } + /** * Starts polling the connectivity check endpoint - * @param {number} delay How long to delay until the first poll. + * @param delay - How long to delay until the first poll. * defaults to a short, randomised interval (to prevent * tight-looping if /versions succeeds but /sync etc. fail). - * @return {promise} which resolves once the connection returns + * @returns which resolves once the connection returns */ - - startKeepAlives(delay) { if (delay === undefined) { delay = 2000 + Math.floor(Math.random() * 5000); } - if (this.keepAliveTimer !== null) { clearTimeout(this.keepAliveTimer); } - if (delay > 0) { this.keepAliveTimer = setTimeout(this.pokeKeepAlive.bind(this), delay); } else { this.pokeKeepAlive(); } - if (!this.connectionReturnedDefer) { this.connectionReturnedDefer = utils.defer(); } - return this.connectionReturnedDefer.promise; } + /** * Make a dummy call to /_matrix/client/versions, to see if the HS is * reachable. @@ -1409,26 +1322,24 @@ * On failure, schedules a call back to itself. On success, resolves * this.connectionReturnedDefer. * - * @param {boolean} connDidFail True if a connectivity failure has been detected. Optional. + * @param connDidFail - True if a connectivity failure has been detected. Optional. */ - - pokeKeepAlive(connDidFail = false) { const success = () => { clearTimeout(this.keepAliveTimer); - if (this.connectionReturnedDefer) { this.connectionReturnedDefer.resolve(connDidFail); - this.connectionReturnedDefer = null; + this.connectionReturnedDefer = undefined; } }; - - this.client.http.request(undefined, // callback - _httpApi.Method.Get, "/_matrix/client/versions", undefined, // queryParams - undefined, // data + this.client.http.request(_httpApi.Method.Get, "/_matrix/client/versions", undefined, + // queryParams + undefined, + // data { - prefix: '', - localTimeoutMs: 15 * 1000 + prefix: "", + localTimeoutMs: 15 * 1000, + abortSignal: this.abortController?.signal }).then(() => { success(); }, err => { @@ -1441,89 +1352,67 @@ this.keepAliveTimer = setTimeout(success, 2000); } else { connDidFail = true; - this.keepAliveTimer = setTimeout(this.pokeKeepAlive.bind(this, connDidFail), 5000 + Math.floor(Math.random() * 5000)); // A keepalive has failed, so we emit the + this.keepAliveTimer = setTimeout(this.pokeKeepAlive.bind(this, connDidFail), 5000 + Math.floor(Math.random() * 5000)); + // A keepalive has failed, so we emit the // error state (whether or not this is the // first failure). // Note we do this after setting the timer: // this lets the unit tests advance the mock // clock when they get the error. - this.updateSyncState(SyncState.Error, { error: err }); } }); } - /** - * @param {Object} obj - * @return {Object[]} - */ - - mapSyncResponseToRoomArray(obj) { // Maps { roomid: {stuff}, roomid: {stuff} } // to // [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}] const client = this.client; - return Object.keys(obj).map(roomId => { + return Object.keys(obj).filter(k => !(0, utils.unsafeProp)(k)).map(roomId => { const arrObj = obj[roomId]; let room = client.store.getRoom(roomId); let isBrandNewRoom = false; - if (!room) { room = this.createRoom(roomId); isBrandNewRoom = true; } - arrObj.room = room; arrObj.isBrandNewRoom = isBrandNewRoom; return arrObj; }); } - /** - * @param {Object} obj - * @param {Room} room - * @param {boolean} decrypt - * @return {MatrixEvent[]} - */ - - mapSyncEventsFormat(obj, room, decrypt = true) { if (!obj || !Array.isArray(obj.events)) { return []; } - const mapper = this.client.getEventMapper({ decrypt }); - return obj.events.map(function (e) { + return obj.events.filter(utils.noUnsafeEventProps).map(function (e) { if (room) { - e["room_id"] = room.roomId; + e.room_id = room.roomId; } - return mapper(e); }); } + /** - * @param {Room} room */ - - resolveInvites(room) { if (!room || !this.opts.resolveInvitesToProfiles) { return; } - - const client = this.client; // For each invited room member we want to give them a displayname/avatar url + const client = this.client; + // For each invited room member we want to give them a displayname/avatar url // if they have one (the m.room.member invites don't contain this). - room.getMembersWithMembership("invite").forEach(function (member) { - if (member._requestedProfileInfo) return; - member._requestedProfileInfo = true; // try to get a cached copy first. - + if (member.requestedProfileInfo) return; + member.requestedProfileInfo = true; + // try to get a cached copy first. const user = client.getUser(member.userId); let promise; - if (user) { promise = Promise.resolve({ avatar_url: user.avatarUrl, @@ -1532,42 +1421,38 @@ } else { promise = client.getProfileInfo(member.userId); } - promise.then(function (info) { // slightly naughty by doctoring the invite event but this means all // the code paths remain the same between invite/join display name stuff // which is a worthy trade-off for some minor pollution. const inviteEvent = member.events.member; - - if (inviteEvent.getContent().membership !== "invite") { + if (inviteEvent?.getContent().membership !== "invite") { // between resolving and now they have since joined, so don't clobber return; } - inviteEvent.getContent().avatar_url = info.avatar_url; - inviteEvent.getContent().displayname = info.displayname; // fire listeners - + inviteEvent.getContent().displayname = info.displayname; + // fire listeners member.setMembershipEvent(inviteEvent, room.currentState); - }, function (err) {// OH WELL. + }, function (err) { + // OH WELL. }); }); } + /** - * @param {Room} room - * @param {MatrixEvent[]} stateEventList A list of state events. This is the state + * Injects events into a room's model. + * @param stateEventList - A list of state events. This is the state * at the *START* of the timeline list if it is supplied. - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events, including threaded. Lower index - * @param {boolean} fromCache whether the sync response came from cache + * @param timelineEventList - A list of timeline events, including threaded. Lower index * is earlier in time. Higher index is later. + * @param fromCache - whether the sync response came from cache */ - - - async processRoomEvents(room, stateEventList, timelineEventList, fromCache = false) { + async injectRoomEvents(room, stateEventList, timelineEventList, fromCache = false) { // If there are no events in the timeline yet, initialise it with // the given state events const liveTimeline = room.getLiveTimeline(); const timelineWasEmpty = liveTimeline.getEvents().length == 0; - if (timelineWasEmpty) { // Passing these events into initialiseState will freeze them, so we need // to compute and cache the push actions for them now, otherwise sync dies @@ -1580,13 +1465,13 @@ for (const ev of stateEventList) { this.client.getPushActionsForEvent(ev); } - liveTimeline.initialiseState(stateEventList, { timelineWasEmpty }); } + this.resolveInvites(room); - this.resolveInvites(room); // recalculate the room name at this point as adding events to the timeline + // recalculate the room name at this point as adding events to the timeline // may make notifications appear which should have the right name. // XXX: This looks suspect: we'll end up recalculating the room once here // and then again after adding events (processSyncResponse calls it after @@ -1595,8 +1480,9 @@ // a recalculation (like m.room.name) we won't recalculate until we've // finished adding all the events, which will cause the notification to have // the old room name rather than the new one. + room.recalculate(); - room.recalculate(); // If the timeline wasn't empty, we process the state events here: they're + // If the timeline wasn't empty, we process the state events here: they're // defined as updates to the state before the start of the timeline, so this // starts to roll the state forward. // XXX: That's what we *should* do, but this can happen if we were previously @@ -1606,91 +1492,78 @@ // very wrong because there could be events in the timeline that diverge the // state, in which case this is going to leave things out of sync. However, // for now I think it;s best to behave the same as the code has done previously. - if (!timelineWasEmpty) { // XXX: As above, don't do this... //room.addLiveEvents(stateEventList || []); // Do this instead... room.oldState.setStateEvents(stateEventList || []); room.currentState.setStateEvents(stateEventList || []); - } // Execute the timeline events. This will continue to diverge the current state + } + + // Execute the timeline events. This will continue to diverge the current state // if the timeline has any state events in it. // This also needs to be done before running push rules on the events as they need // to be decorated with sender etc. - - room.addLiveEvents(timelineEventList || [], { fromCache, timelineWasEmpty }); this.client.processBeaconEvents(room, timelineEventList); } + /** * Takes a list of timelineEvents and adds and adds to notifEvents * as appropriate. * This must be called after the room the events belong to has been stored. * - * @param {Room} room - * @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index + * @param timelineEventList - A list of timeline events. Lower index * is earlier in time. Higher index is later. */ - - processEventsForNotifs(room, timelineEventList) { // gather our notifications into this.notifEvents if (this.client.getNotifTimelineSet()) { - for (let i = 0; i < timelineEventList.length; i++) { - const pushActions = this.client.getPushActionsForEvent(timelineEventList[i]); - - if (pushActions && pushActions.notify && pushActions.tweaks && pushActions.tweaks.highlight) { - this.notifEvents.push(timelineEventList[i]); + for (const event of timelineEventList) { + const pushActions = this.client.getPushActionsForEvent(event); + if (pushActions?.notify && pushActions.tweaks?.highlight) { + this.notifEvents.push(event); } } } } - /** - * @return {string} - */ - - getGuestFilter() { // Dev note: This used to be conditional to return a filter of 20 events maximum, but // the condition never went to the other branch. This is now hardcoded. return "{}"; } + /** * Sets the sync state and emits an event to say so - * @param {String} newState The new state string - * @param {Object} data Object of additional data to emit in the event + * @param newState - The new state string + * @param data - Object of additional data to emit in the event */ - - updateSyncState(newState, data) { const old = this.syncState; this.syncState = newState; this.syncStateData = data; this.client.emit(_client.ClientEvent.Sync, this.syncState, old, data); } + /** * Event handler for the 'online' event * This event is generally unreliable and precise behaviour * varies between browsers, so we poll for connectivity too, * but this might help us reconnect a little faster. */ - - } - exports.SyncApi = SyncApi; - function createNewUser(client, userId) { const user = new _user.User(userId); client.reEmitter.reEmit(user, [_user.UserEvent.AvatarUrl, _user.UserEvent.DisplayName, _user.UserEvent.Presence, _user.UserEvent.CurrentlyActive, _user.UserEvent.LastPresenceTs]); return user; -} // /!\ This function is not intended for public use! It's only exported from -// here in order to share some common logic with sliding-sync-sdk.ts. - +} +// /!\ This function is not intended for public use! It's only exported from +// here in order to share some common logic with sliding-sync-sdk.ts. function _createAndReEmitRoom(client, roomId, opts) { const { timelineSupport @@ -1700,11 +1573,12 @@ pendingEventOrdering: opts.pendingEventOrdering, timelineSupport }); - client.reEmitter.reEmit(room, [_room.RoomEvent.Name, _room.RoomEvent.Redaction, _room.RoomEvent.RedactionCancelled, _room.RoomEvent.Receipt, _room.RoomEvent.Tags, _room.RoomEvent.LocalEchoUpdated, _room.RoomEvent.AccountData, _room.RoomEvent.MyMembership, _room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset, _roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update, _beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]); // We need to add a listener for RoomState.members in order to hook them - // correctly. + client.reEmitter.reEmit(room, [_room.RoomEvent.Name, _room.RoomEvent.Redaction, _room.RoomEvent.RedactionCancelled, _room.RoomEvent.Receipt, _room.RoomEvent.Tags, _room.RoomEvent.LocalEchoUpdated, _room.RoomEvent.AccountData, _room.RoomEvent.MyMembership, _room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset, _roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update, _beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]); + // We need to add a listener for RoomState.members in order to hook them + // correctly. room.on(_roomState.RoomStateEvent.NewMember, (event, state, member) => { - member.user = client.getUser(member.userId); + member.user = client.getUser(member.userId) ?? undefined; client.reEmitter.reEmit(member, [_roomMember.RoomMemberEvent.Name, _roomMember.RoomMemberEvent.Typing, _roomMember.RoomMemberEvent.PowerLevel, _roomMember.RoomMemberEvent.Membership]); }); return room; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/timeline-window.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,30 +4,28 @@ value: true }); exports.TimelineWindow = exports.TimelineIndex = void 0; - var _eventTimeline = require("./models/event-timeline"); - var _logger = require("./logger"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /** - * @private + * @internal */ const DEBUG = false; + /** - * @private + * @internal */ - +/* istanbul ignore next */ const debuglog = DEBUG ? _logger.logger.log.bind(_logger.logger) : function () {}; + /** * the number of times we ask the server for more events before giving up * - * @private + * @internal */ - const DEFAULT_PAGINATE_LOOP_LIMIT = 5; - class TimelineWindow { // these will be TimelineIndex objects; they delineate the 'start' and // 'end' of the window. @@ -37,158 +35,128 @@ /** * Construct a TimelineWindow. * - *

This abstracts the separate timelines in a Matrix {@link - * module:models/room|Room} into a single iterable thing. It keeps track of - * the start and endpoints of the window, which can be advanced with the help + *

This abstracts the separate timelines in a Matrix {@link Room} into a single iterable thing. + * It keeps track of the start and endpoints of the window, which can be advanced with the help * of pagination requests. * - *

Before the window is useful, it must be initialised by calling {@link - * module:timeline-window~TimelineWindow#load|load}. + *

Before the window is useful, it must be initialised by calling {@link TimelineWindow#load}. * *

Note that the window will not automatically extend itself when new events - * are received from /sync; you should arrange to call {@link - * module:timeline-window~TimelineWindow#paginate|paginate} on {@link - * module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events. + * are received from /sync; you should arrange to call {@link TimelineWindow#paginate} + * on {@link RoomEvent.Timeline} events. * - * @param {MatrixClient} client MatrixClient to be used for context/pagination + * @param client - MatrixClient to be used for context/pagination * requests. * - * @param {EventTimelineSet} timelineSet The timelineSet to track + * @param timelineSet - The timelineSet to track * - * @param {Object} [opts] Configuration options for this window - * - * @param {number} [opts.windowLimit = 1000] maximum number of events to keep - * in the window. If more events are retrieved via pagination requests, - * excess events will be dropped from the other end of the window. - * - * @constructor + * @param opts - Configuration options for this window */ constructor(client, timelineSet, opts = {}) { this.client = client; this.timelineSet = timelineSet; - _defineProperty(this, "windowLimit", void 0); - - _defineProperty(this, "start", null); - - _defineProperty(this, "end", null); - + _defineProperty(this, "start", void 0); + _defineProperty(this, "end", void 0); _defineProperty(this, "eventCount", 0); - this.windowLimit = opts.windowLimit || 1000; } + /** * Initialise the window to point at a given event, or the live timeline * - * @param {string} [initialEventId] If given, the window will contain the + * @param initialEventId - If given, the window will contain the * given event - * @param {number} [initialWindowSize = 20] Size of the initial window - * - * @return {Promise} + * @param initialWindowSize - Size of the initial window */ - - load(initialEventId, initialWindowSize = 20) { // given an EventTimeline, find the event we were looking for, and initialise our // fields so that the event in question is in the middle of the window. const initFields = timeline => { + if (!timeline) { + throw new Error("No timeline given to initFields"); + } let eventIndex; const events = timeline.getEvents(); - if (!initialEventId) { // we were looking for the live timeline: initialise to the end eventIndex = events.length; } else { eventIndex = events.findIndex(e => e.getId() === initialEventId); - if (eventIndex < 0) { throw new Error("getEventTimeline result didn't include requested event"); } } - const endIndex = Math.min(events.length, eventIndex + Math.ceil(initialWindowSize / 2)); const startIndex = Math.max(0, endIndex - initialWindowSize); this.start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex()); this.end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex()); this.eventCount = endIndex - startIndex; - }; // We avoid delaying the resolution of the promise by a reactor tick if we already have the data we need, - // which is important to keep room-switching feeling snappy. - - - if (initialEventId) { - const timeline = this.timelineSet.getTimelineForEvent(initialEventId); - - if (timeline) { - // hot-path optimization to save a reactor tick by replicating the sync check getTimelineForEvent does. - initFields(timeline); - return Promise.resolve(); - } + }; + // We avoid delaying the resolution of the promise by a reactor tick if we already have the data we need, + // which is important to keep room-switching feeling snappy. + if (this.timelineSet.getTimelineForEvent(initialEventId)) { + initFields(this.timelineSet.getTimelineForEvent(initialEventId)); + return Promise.resolve(); + } else if (initialEventId) { return this.client.getEventTimeline(this.timelineSet, initialEventId).then(initFields); } else { - const tl = this.timelineSet.getLiveTimeline(); - initFields(tl); + initFields(this.timelineSet.getLiveTimeline()); return Promise.resolve(); } } + /** * Get the TimelineIndex of the window in the given direction. * - * @param {string} direction EventTimeline.BACKWARDS to get the TimelineIndex + * @param direction - EventTimeline.BACKWARDS to get the TimelineIndex * at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at * the end. * - * @return {TimelineIndex} The requested timeline index if one exists, null + * @returns The requested timeline index if one exists, null * otherwise. */ - - getTimelineIndex(direction) { if (direction == _eventTimeline.EventTimeline.BACKWARDS) { - return this.start; + return this.start ?? null; } else if (direction == _eventTimeline.EventTimeline.FORWARDS) { - return this.end; + return this.end ?? null; } else { throw new Error("Invalid direction '" + direction + "'"); } } + /** * Try to extend the window using events that are already in the underlying * TimelineIndex. * - * @param {string} direction EventTimeline.BACKWARDS to try extending it + * @param direction - EventTimeline.BACKWARDS to try extending it * backwards; EventTimeline.FORWARDS to try extending it forwards. - * @param {number} size number of events to try to extend by. + * @param size - number of events to try to extend by. * - * @return {boolean} true if the window was extended, false otherwise. + * @returns true if the window was extended, false otherwise. */ - - extend(direction, size) { const tl = this.getTimelineIndex(direction); - if (!tl) { debuglog("TimelineWindow: no timeline yet"); return false; } - const count = direction == _eventTimeline.EventTimeline.BACKWARDS ? tl.retreat(size) : tl.advance(size); - if (count) { this.eventCount += count; - debuglog("TimelineWindow: increased cap by " + count + " (now " + this.eventCount + ")"); // remove some events from the other end, if necessary - + debuglog("TimelineWindow: increased cap by " + count + " (now " + this.eventCount + ")"); + // remove some events from the other end, if necessary const excess = this.eventCount - this.windowLimit; - if (excess > 0) { this.unpaginate(excess, direction != _eventTimeline.EventTimeline.BACKWARDS); } - return true; } - return false; } + /** * Check if this window can be extended * @@ -197,21 +165,17 @@ * necessarily mean that there are more events available in that direction at * this time. * - * @param {string} direction EventTimeline.BACKWARDS to check if we can + * @param direction - EventTimeline.BACKWARDS to check if we can * paginate backwards; EventTimeline.FORWARDS to check if we can go forwards * - * @return {boolean} true if we can paginate in the given direction + * @returns true if we can paginate in the given direction */ - - canPaginate(direction) { const tl = this.getTimelineIndex(direction); - if (!tl) { debuglog("TimelineWindow: no timeline yet"); return false; } - if (direction == _eventTimeline.EventTimeline.BACKWARDS) { if (tl.index > tl.minIndex()) { return true; @@ -221,79 +185,74 @@ return true; } } - - return Boolean(tl.timeline.getNeighbouringTimeline(direction) || tl.timeline.getPaginationToken(direction) !== null); + const hasNeighbouringTimeline = tl.timeline.getNeighbouringTimeline(direction); + const paginationToken = tl.timeline.getPaginationToken(direction); + return Boolean(hasNeighbouringTimeline) || Boolean(paginationToken); } + /** * Attempt to extend the window * - * @param {string} direction EventTimeline.BACKWARDS to extend the window + * @param direction - EventTimeline.BACKWARDS to extend the window * backwards (towards older events); EventTimeline.FORWARDS to go forwards. * - * @param {number} size number of events to try to extend by. If fewer than this + * @param size - number of events to try to extend by. If fewer than this * number are immediately available, then we return immediately rather than * making an API call. * - * @param {boolean} [makeRequest = true] whether we should make API calls to + * @param makeRequest - whether we should make API calls to * fetch further events if we don't have any at all. (This has no effect if * the room already knows about additional events in the relevant direction, * even if there are fewer than 'size' of them, as we will just return those * we already know about.) * - * @param {number} [requestLimit = 5] limit for the number of API requests we + * @param requestLimit - limit for the number of API requests we * should make. * - * @return {Promise} Resolves to a boolean which is true if more events + * @returns Promise which resolves to a boolean which is true if more events * were successfully retrieved. */ - - - paginate(direction, size, makeRequest = true, requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT) { + async paginate(direction, size, makeRequest = true, requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT) { // Either wind back the message cap (if there are enough events in the // timeline to do so), or fire off a pagination request. const tl = this.getTimelineIndex(direction); - if (!tl) { debuglog("TimelineWindow: no timeline yet"); - return Promise.resolve(false); + return false; } - if (tl.pendingPaginate) { return tl.pendingPaginate; - } // try moving the cap - + } + // try moving the cap if (this.extend(direction, size)) { - return Promise.resolve(true); + return true; } - if (!makeRequest || requestLimit === 0) { // todo: should we return something different to indicate that there // might be more events out there, but we haven't found them yet? - return Promise.resolve(false); - } // try making a pagination request - + return false; + } + // try making a pagination request const token = tl.timeline.getPaginationToken(direction); - - if (token === null) { + if (!token) { debuglog("TimelineWindow: no token"); - return Promise.resolve(false); + return false; } - debuglog("TimelineWindow: starting request"); const prom = this.client.paginateEventTimeline(tl.timeline, { backwards: direction == _eventTimeline.EventTimeline.BACKWARDS, limit: size }).finally(function () { - tl.pendingPaginate = null; + tl.pendingPaginate = undefined; }).then(r => { debuglog("TimelineWindow: request completed with result " + r); - if (!r) { - // end of timeline - return false; - } // recurse to advance the index into the results. + return this.paginate(direction, size, false, 0); + } + + // recurse to advance the index into the results. // // If we don't get any new events, we want to make sure we keep asking // the server for events for as long as we have a valid pagination @@ -305,62 +264,61 @@ // server to make its mind up about whether there are other events, // because it gives a bad user experience // (https://github.com/vector-im/vector-web/issues/1204). - - return this.paginate(direction, size, true, requestLimit - 1); }); tl.pendingPaginate = prom; return prom; } + /** * Remove `delta` events from the start or end of the timeline. * - * @param {number} delta number of events to remove from the timeline - * @param {boolean} startOfTimeline if events should be removed from the start + * @param delta - number of events to remove from the timeline + * @param startOfTimeline - if events should be removed from the start * of the timeline. */ - - unpaginate(delta, startOfTimeline) { - const tl = startOfTimeline ? this.start : this.end; // sanity-check the delta + const tl = startOfTimeline ? this.start : this.end; + if (!tl) { + throw new Error(`Attempting to unpaginate startOfTimeline=${startOfTimeline} but don't have this direction`); + } + // sanity-check the delta if (delta > this.eventCount || delta < 0) { - throw new Error("Attemting to unpaginate " + delta + " events, but " + "only have " + this.eventCount + " in the timeline"); + throw new Error(`Attemting to unpaginate ${delta} events, but only have ${this.eventCount} in the timeline`); } - while (delta > 0) { const count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta); - if (count <= 0) { // sadness. This shouldn't be possible. throw new Error("Unable to unpaginate any further, but still have " + this.eventCount + " events"); } - delta -= count; this.eventCount -= count; debuglog("TimelineWindow.unpaginate: dropped " + count + " (now " + this.eventCount + ")"); } } + /** * Get a list of the events currently in the window * - * @return {MatrixEvent[]} the events in the window + * @returns the events in the window */ - - getEvents() { if (!this.start) { // not yet loaded return []; } + const result = []; - const result = []; // iterate through each timeline between this.start and this.end + // iterate through each timeline between this.start and this.end // (inclusive). - - let timeline = this.start.timeline; // eslint-disable-next-line no-constant-condition - + let timeline = this.start.timeline; + // eslint-disable-next-line no-constant-condition while (true) { - const events = timeline.getEvents(); // For the first timeline in the chain, we want to start at + const events = timeline.getEvents(); + + // For the first timeline in the chain, we want to start at // this.start.index. For the last timeline in the chain, we want to // stop before this.end.index. Otherwise, we want to copy all of the // events in the timeline. @@ -368,90 +326,73 @@ // (Note that both this.start.index and this.end.index are relative // to their respective timelines' BaseIndex). // - let startIndex = 0; let endIndex = events.length; - if (timeline === this.start.timeline) { startIndex = this.start.index + timeline.getBaseIndex(); } - - if (timeline === this.end.timeline) { + if (timeline === this.end?.timeline) { endIndex = this.end.index + timeline.getBaseIndex(); } - for (let i = startIndex; i < endIndex; i++) { result.push(events[i]); - } // if we're not done, iterate to the next timeline. - + } - if (timeline === this.end.timeline) { + // if we're not done, iterate to the next timeline. + if (timeline === this.end?.timeline) { break; } else { timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS); } } - return result; } - } + /** - * a thing which contains a timeline reference, and an index into it. - * - * @constructor - * @param {EventTimeline} timeline - * @param {number} index - * @private + * A thing which contains a timeline reference, and an index into it. + * @internal */ - - exports.TimelineWindow = TimelineWindow; - class TimelineIndex { // index: the indexes are relative to BaseIndex, so could well be negative. constructor(timeline, index) { this.timeline = timeline; this.index = index; - _defineProperty(this, "pendingPaginate", void 0); } + /** - * @return {number} the minimum possible value for the index in the current + * @returns the minimum possible value for the index in the current * timeline */ - - minIndex() { return this.timeline.getBaseIndex() * -1; } + /** - * @return {number} the maximum possible value for the index in the current + * @returns the maximum possible value for the index in the current * timeline (exclusive - ie, it actually returns one more than the index * of the last element). */ - - maxIndex() { return this.timeline.getEvents().length - this.timeline.getBaseIndex(); } + /** * Try move the index forward, or into the neighbouring timeline * - * @param {number} delta number of events to advance by - * @return {number} number of events successfully advanced by + * @param delta - number of events to advance by + * @returns number of events successfully advanced by */ - - advance(delta) { if (!delta) { return 0; - } // first try moving the index in the current timeline. See if there is room - // to do so. - + } + // first try moving the index in the current timeline. See if there is room + // to do so. let cappedDelta; - if (delta < 0) { // we want to wind the index backwards. // @@ -459,7 +400,6 @@ // is the amount of room we have to wind back the index in the current // timeline. We cap delta to this quantity. cappedDelta = Math.max(delta, this.minIndex() - this.index); - if (cappedDelta < 0) { this.index += cappedDelta; return cappedDelta; @@ -471,46 +411,39 @@ // is the amount of room we have to wind forward the index in the current // timeline. We cap delta to this quantity. cappedDelta = Math.min(delta, this.maxIndex() - this.index); - if (cappedDelta > 0) { this.index += cappedDelta; return cappedDelta; } - } // the index is already at the start/end of the current timeline. + } + + // the index is already at the start/end of the current timeline. // // next see if there is a neighbouring timeline to switch to. - - const neighbour = this.timeline.getNeighbouringTimeline(delta < 0 ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS); - if (neighbour) { this.timeline = neighbour; - if (delta < 0) { this.index = this.maxIndex(); } else { this.index = this.minIndex(); } + debuglog("paginate: switched to new neighbour"); - debuglog("paginate: switched to new neighbour"); // recurse, using the next timeline - + // recurse, using the next timeline return this.advance(delta); } - return 0; } + /** * Try move the index backwards, or into the neighbouring timeline * - * @param {number} delta number of events to retreat by - * @return {number} number of events successfully retreated by + * @param delta - number of events to retreat by + * @returns number of events successfully retreated by */ - - retreat(delta) { return this.advance(delta * -1) * -1; } - } - exports.TimelineIndex = TimelineIndex; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/ToDeviceMessageQueue.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/ToDeviceMessageQueue.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/ToDeviceMessageQueue.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/ToDeviceMessageQueue.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,41 +4,35 @@ value: true }); exports.ToDeviceMessageQueue = void 0; - +var _event = require("./@types/event"); var _logger = require("./logger"); - +var _client = require("./client"); var _scheduler = require("./scheduler"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +var _sync = require("./sync"); +var _utils = require("./utils"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const MAX_BATCH_SIZE = 20; + /** * Maintains a queue of outgoing to-device messages, sending them * as soon as the homeserver is reachable. */ - class ToDeviceMessageQueue { constructor(client) { this.client = client; - _defineProperty(this, "sending", false); - _defineProperty(this, "running", true); - _defineProperty(this, "retryTimeout", null); - _defineProperty(this, "retryAttempts", 0); - _defineProperty(this, "sendQueue", async () => { if (this.retryTimeout !== null) clearTimeout(this.retryTimeout); this.retryTimeout = null; if (this.sending || !this.running) return; - _logger.logger.debug("Attempting to send queued to-device messages"); - this.sending = true; let headBatch; - try { while (this.running) { headBatch = await this.client.store.getOldestToDeviceBatch(); @@ -46,86 +40,81 @@ await this.sendBatch(headBatch); await this.client.store.removeToDeviceBatch(headBatch.id); this.retryAttempts = 0; - } // Make sure we're still running after the async tasks: if not, stop. - + } + // Make sure we're still running after the async tasks: if not, stop. if (!this.running) return; - _logger.logger.debug("All queued to-device messages sent"); } catch (e) { - ++this.retryAttempts; // eslint-disable-next-line @typescript-eslint/naming-convention + ++this.retryAttempts; + // eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line new-cap - const retryDelay = _scheduler.MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, e); - if (retryDelay === -1) { // the scheduler function doesn't differentiate between fatal errors and just getting // bored and giving up for now if (Math.floor(e.httpStatus / 100) === 4) { _logger.logger.error("Fatal error when sending to-device message - dropping to-device batch!", e); - await this.client.store.removeToDeviceBatch(headBatch.id); } else { _logger.logger.info("Automatic retry limit reached for to-device messages."); } - return; } - _logger.logger.info(`Failed to send batch of to-device messages. Will retry in ${retryDelay}ms`, e); - this.retryTimeout = setTimeout(this.sendQueue, retryDelay); } finally { this.sending = false; } }); + _defineProperty(this, "onResumedSync", (state, oldState) => { + if (state === _sync.SyncState.Syncing && oldState !== _sync.SyncState.Syncing) { + _logger.logger.info(`Resuming queue after resumed sync`); + this.sendQueue(); + } + }); } - start() { this.running = true; this.sendQueue(); + this.client.on(_client.ClientEvent.Sync, this.onResumedSync); } - stop() { this.running = false; if (this.retryTimeout !== null) clearTimeout(this.retryTimeout); this.retryTimeout = null; + this.client.removeListener(_client.ClientEvent.Sync, this.onResumedSync); } - async queueBatch(batch) { const batches = []; - for (let i = 0; i < batch.batch.length; i += MAX_BATCH_SIZE) { - batches.push({ + const batchWithTxnId = { eventType: batch.eventType, batch: batch.batch.slice(i, i + MAX_BATCH_SIZE), txnId: this.client.makeTxnId() - }); + }; + batches.push(batchWithTxnId); + const msgmap = batchWithTxnId.batch.map(msg => `${msg.userId}/${msg.deviceId} (msgid ${msg.payload[_event.ToDeviceMessageId]})`); + _logger.logger.info(`Enqueuing batch of to-device messages. type=${batch.eventType} txnid=${batchWithTxnId.txnId}`, msgmap); } - await this.client.store.saveToDeviceBatches(batches); this.sendQueue(); } - /** * Attempts to send a batch of to-device messages. */ async sendBatch(batch) { - const contentMap = {}; - + const contentMap = new _utils.MapWithDefault(() => new Map()); for (const item of batch.batch) { - if (!contentMap[item.userId]) { - contentMap[item.userId] = {}; - } - - contentMap[item.userId][item.deviceId] = item.payload; + contentMap.getOrCreate(item.userId).set(item.deviceId, item.payload); } - - _logger.logger.info(`Sending batch of ${batch.batch.length} to-device messages with ID ${batch.id}`); - + _logger.logger.info(`Sending batch of ${batch.batch.length} to-device messages with ID ${batch.id} and txnId ${batch.txnId}`); await this.client.sendToDevice(batch.eventType, contentMap, batch.txnId); } + /** + * Listen to sync state changes and automatically resend any pending events + * once syncing is resumed + */ } - exports.ToDeviceMessageQueue = ToDeviceMessageQueue; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/another-json.d.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/another-json.d.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/another-json.d.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/another-json.d.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1 @@ +"use strict"; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/another-json.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/another-json.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/another-json.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/another-json.js 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -"use strict"; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/auth.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/auth.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/auth.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/auth.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.SSOAction = exports.IdentityProviderBrand = void 0; - +exports.SSOAction = exports.IdentityProviderBrand = exports.DELEGATED_OIDC_COMPATIBILITY = void 0; +var _NamespacedValue = require("../NamespacedValue"); /* Copyright 2022 The Matrix.org Foundation C.I.C. @@ -20,26 +20,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -// disable lint because these are wire responses - -/* eslint-disable camelcase */ - -/** - * Represents a response to the CSAPI `/refresh` endpoint. - */ - -/* eslint-enable camelcase */ -/** - * Response to GET login flows as per https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3login - */ +const DELEGATED_OIDC_COMPATIBILITY = new _NamespacedValue.UnstableValue("delegated_oidc_compatibility", "org.matrix.msc3824.delegated_oidc_compatibility"); /** - * Representation of SSO flow as per https://spec.matrix.org/latest/client-server-api/#client-login-via-sso + * Representation of SSO flow as per https://spec.matrix.org/v1.3/client-server-api/#client-login-via-sso */ +exports.DELEGATED_OIDC_COMPATIBILITY = DELEGATED_OIDC_COMPATIBILITY; let IdentityProviderBrand; exports.IdentityProviderBrand = IdentityProviderBrand; - (function (IdentityProviderBrand) { IdentityProviderBrand["Gitlab"] = "gitlab"; IdentityProviderBrand["Github"] = "github"; @@ -48,11 +37,14 @@ IdentityProviderBrand["Facebook"] = "facebook"; IdentityProviderBrand["Twitter"] = "twitter"; })(IdentityProviderBrand || (exports.IdentityProviderBrand = IdentityProviderBrand = {})); - /* eslint-enable camelcase */ let SSOAction; +/** + * The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882) + * `m.login.token` issuance request. + * Note that this is UNSTABLE and subject to breaking changes without notice. + */ exports.SSOAction = SSOAction; - (function (SSOAction) { SSOAction["LOGIN"] = "login"; SSOAction["REGISTER"] = "register"; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/beacon.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/beacon.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/beacon.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/beacon.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,7 @@ value: true }); exports.M_BEACON_INFO = exports.M_BEACON = void 0; - var _NamespacedValue = require("../NamespacedValue"); - /* Copyright 2022 The Matrix.org Foundation C.I.C. @@ -39,7 +37,8 @@ * To achieve an arbitrary number of only owner-writable state events * we introduce a variable suffix to the event type * - * Eg + * @example + * ``` * { * "type": "m.beacon_info.@matthew:matrix.org.1", * "state_key": "@matthew:matrix.org", @@ -62,6 +61,7 @@ * // more content as described below * } * } + * ``` */ /** diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/event.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/event.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/event.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/event.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,10 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.UNSTABLE_MSC3089_TREE_SUBTYPE = exports.UNSTABLE_MSC3089_LEAF = exports.UNSTABLE_MSC3089_BRANCH = exports.UNSTABLE_MSC3088_PURPOSE = exports.UNSTABLE_MSC3088_ENABLED = exports.UNSTABLE_MSC2716_MARKER = exports.UNSTABLE_ELEMENT_FUNCTIONAL_USERS = exports.RoomType = exports.RoomCreateTypeField = exports.RelationType = exports.MsgType = exports.EventType = exports.EVENT_VISIBILITY_CHANGE_TYPE = void 0; - +exports.UNSTABLE_MSC3089_TREE_SUBTYPE = exports.UNSTABLE_MSC3089_LEAF = exports.UNSTABLE_MSC3089_BRANCH = exports.UNSTABLE_MSC3088_PURPOSE = exports.UNSTABLE_MSC3088_ENABLED = exports.UNSTABLE_MSC2716_MARKER = exports.UNSTABLE_ELEMENT_FUNCTIONAL_USERS = exports.ToDeviceMessageId = exports.RoomType = exports.RoomCreateTypeField = exports.RelationType = exports.PUSHER_ENABLED = exports.PUSHER_DEVICE_ID = exports.MsgType = exports.MSC3912_RELATION_BASED_REDACTIONS_PROP = exports.LOCAL_NOTIFICATION_SETTINGS_PREFIX = exports.EventType = exports.EVENT_VISIBILITY_CHANGE_TYPE = void 0; var _NamespacedValue = require("../NamespacedValue"); - /* Copyright 2020 The Matrix.org Foundation C.I.C. @@ -24,7 +22,6 @@ */ let EventType; exports.EventType = EventType; - (function (EventType) { EventType["RoomCanonicalAlias"] = "m.room.canonical_alias"; EventType["RoomCreate"] = "m.room.create"; @@ -41,7 +38,7 @@ EventType["RoomGuestAccess"] = "m.room.guest_access"; EventType["RoomServerAcl"] = "m.room.server_acl"; EventType["RoomTombstone"] = "m.room.tombstone"; - EventType["RoomAliases"] = "m.room.aliases"; + EventType["RoomPredecessor"] = "org.matrix.msc3946.room_predecessor"; EventType["SpaceChild"] = "m.space.child"; EventType["SpaceParent"] = "m.space.parent"; EventType["RoomRedaction"] = "m.room.redaction"; @@ -65,8 +62,12 @@ EventType["KeyVerificationCancel"] = "m.key.verification.cancel"; EventType["KeyVerificationMac"] = "m.key.verification.mac"; EventType["KeyVerificationDone"] = "m.key.verification.done"; + EventType["KeyVerificationKey"] = "m.key.verification.key"; + EventType["KeyVerificationAccept"] = "m.key.verification.accept"; + EventType["KeyVerificationReady"] = "m.key.verification.ready"; EventType["RoomMessageFeedback"] = "m.room.message.feedback"; EventType["Reaction"] = "m.reaction"; + EventType["PollStart"] = "org.matrix.msc3381.poll.start"; EventType["Typing"] = "m.typing"; EventType["Receipt"] = "m.receipt"; EventType["Presence"] = "m.presence"; @@ -80,21 +81,19 @@ EventType["RoomKeyRequest"] = "m.room_key_request"; EventType["ForwardedRoomKey"] = "m.forwarded_room_key"; EventType["Dummy"] = "m.dummy"; + EventType["GroupCallPrefix"] = "org.matrix.msc3401.call"; + EventType["GroupCallMemberPrefix"] = "org.matrix.msc3401.call.member"; })(EventType || (exports.EventType = EventType = {})); - let RelationType; exports.RelationType = RelationType; - (function (RelationType) { RelationType["Annotation"] = "m.annotation"; RelationType["Replace"] = "m.replace"; RelationType["Reference"] = "m.reference"; RelationType["Thread"] = "m.thread"; })(RelationType || (exports.RelationType = RelationType = {})); - let MsgType; exports.MsgType = MsgType; - (function (MsgType) { MsgType["Text"] = "m.text"; MsgType["Emote"] = "m.emote"; @@ -106,94 +105,128 @@ MsgType["Video"] = "m.video"; MsgType["KeyVerificationRequest"] = "m.key.verification.request"; })(MsgType || (exports.MsgType = MsgType = {})); - const RoomCreateTypeField = "type"; exports.RoomCreateTypeField = RoomCreateTypeField; let RoomType; -/** - * Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088) - * room purpose. Note that this reference is UNSTABLE and subject to breaking changes, - * including its eventual removal. - */ - exports.RoomType = RoomType; - (function (RoomType) { RoomType["Space"] = "m.space"; RoomType["UnstableCall"] = "org.matrix.msc3417.call"; RoomType["ElementVideo"] = "io.element.video"; })(RoomType || (exports.RoomType = RoomType = {})); +const ToDeviceMessageId = "org.matrix.msgid"; +/** + * Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088) + * room purpose. Note that this reference is UNSTABLE and subject to breaking changes, + * including its eventual removal. + */ +exports.ToDeviceMessageId = ToDeviceMessageId; const UNSTABLE_MSC3088_PURPOSE = new _NamespacedValue.UnstableValue("m.room.purpose", "org.matrix.msc3088.purpose"); + /** * Enabled flag for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088) * room purpose. Note that this reference is UNSTABLE and subject to breaking changes, * including its eventual removal. */ - exports.UNSTABLE_MSC3088_PURPOSE = UNSTABLE_MSC3088_PURPOSE; const UNSTABLE_MSC3088_ENABLED = new _NamespacedValue.UnstableValue("m.enabled", "org.matrix.msc3088.enabled"); + /** * Subtype for an [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room. * Note that this reference is UNSTABLE and subject to breaking changes, including its * eventual removal. */ - exports.UNSTABLE_MSC3088_ENABLED = UNSTABLE_MSC3088_ENABLED; const UNSTABLE_MSC3089_TREE_SUBTYPE = new _NamespacedValue.UnstableValue("m.data_tree", "org.matrix.msc3089.data_tree"); + /** * Leaf type for an event in a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room. * Note that this reference is UNSTABLE and subject to breaking changes, including its * eventual removal. */ - exports.UNSTABLE_MSC3089_TREE_SUBTYPE = UNSTABLE_MSC3089_TREE_SUBTYPE; const UNSTABLE_MSC3089_LEAF = new _NamespacedValue.UnstableValue("m.leaf", "org.matrix.msc3089.leaf"); + /** * Branch (Leaf Reference) type for the index approach in a * [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room. Note that this reference is * UNSTABLE and subject to breaking changes, including its eventual removal. */ - exports.UNSTABLE_MSC3089_LEAF = UNSTABLE_MSC3089_LEAF; const UNSTABLE_MSC3089_BRANCH = new _NamespacedValue.UnstableValue("m.branch", "org.matrix.msc3089.branch"); + /** * Marker event type to point back at imported historical content in a room. See * [MSC2716](https://github.com/matrix-org/matrix-spec-proposals/pull/2716). * Note that this reference is UNSTABLE and subject to breaking changes, * including its eventual removal. */ - exports.UNSTABLE_MSC3089_BRANCH = UNSTABLE_MSC3089_BRANCH; const UNSTABLE_MSC2716_MARKER = new _NamespacedValue.UnstableValue("m.room.marker", "org.matrix.msc2716.marker"); + +/** + * Name of the "with_relations" request property for relation based redactions. + * {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912} + */ +exports.UNSTABLE_MSC2716_MARKER = UNSTABLE_MSC2716_MARKER; +const MSC3912_RELATION_BASED_REDACTIONS_PROP = new _NamespacedValue.UnstableValue("with_relations", "org.matrix.msc3912.with_relations"); + /** * Functional members type for declaring a purpose of room members (e.g. helpful bots). * Note that this reference is UNSTABLE and subject to breaking changes, including its * eventual removal. * * Schema (TypeScript): + * ``` * { * service_members?: string[] * } + * ``` * - * Example: + * @example + * ``` * { * "service_members": [ * "@helperbot:localhost", * "@reminderbot:alice.tdl" * ] * } + * ``` */ - -exports.UNSTABLE_MSC2716_MARKER = UNSTABLE_MSC2716_MARKER; +exports.MSC3912_RELATION_BASED_REDACTIONS_PROP = MSC3912_RELATION_BASED_REDACTIONS_PROP; const UNSTABLE_ELEMENT_FUNCTIONAL_USERS = new _NamespacedValue.UnstableValue("io.element.functional_members", "io.element.functional_members"); + /** * A type of message that affects visibility of a message, * as per https://github.com/matrix-org/matrix-doc/pull/3531 * * @experimental */ - exports.UNSTABLE_ELEMENT_FUNCTIONAL_USERS = UNSTABLE_ELEMENT_FUNCTIONAL_USERS; const EVENT_VISIBILITY_CHANGE_TYPE = new _NamespacedValue.UnstableValue("m.visibility", "org.matrix.msc3531.visibility"); -exports.EVENT_VISIBILITY_CHANGE_TYPE = EVENT_VISIBILITY_CHANGE_TYPE; \ No newline at end of file + +/** + * https://github.com/matrix-org/matrix-doc/pull/3881 + * + * @experimental + */ +exports.EVENT_VISIBILITY_CHANGE_TYPE = EVENT_VISIBILITY_CHANGE_TYPE; +const PUSHER_ENABLED = new _NamespacedValue.UnstableValue("enabled", "org.matrix.msc3881.enabled"); + +/** + * https://github.com/matrix-org/matrix-doc/pull/3881 + * + * @experimental + */ +exports.PUSHER_ENABLED = PUSHER_ENABLED; +const PUSHER_DEVICE_ID = new _NamespacedValue.UnstableValue("device_id", "org.matrix.msc3881.device_id"); + +/** + * https://github.com/matrix-org/matrix-doc/pull/3890 + * + * @experimental + */ +exports.PUSHER_DEVICE_ID = PUSHER_DEVICE_ID; +const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new _NamespacedValue.UnstableValue("m.local_notification_settings", "org.matrix.msc3890.local_notification_settings"); +exports.LOCAL_NOTIFICATION_SETTINGS_PREFIX = LOCAL_NOTIFICATION_SETTINGS_PREFIX; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/extensible_events.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/extensible_events.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/extensible_events.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/extensible_events.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.TEXT_NODE_TYPE = void 0; - -var _NamespacedValue = require("../NamespacedValue"); - +exports.REFERENCE_RELATION = exports.M_TEXT = exports.M_MESSAGE = exports.M_HTML = void 0; +exports.isEventTypeSame = isEventTypeSame; +var _matrixEventsSdk = require("matrix-events-sdk"); +var _utilities = require("../extensible_events_v1/utilities"); /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,6 +22,64 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Types for MSC1767: Extensible events in Matrix -const TEXT_NODE_TYPE = new _NamespacedValue.UnstableValue("m.text", "org.matrix.msc1767.text"); -exports.TEXT_NODE_TYPE = TEXT_NODE_TYPE; \ No newline at end of file + +/** + * The namespaced value for m.message + */ +const M_MESSAGE = new _matrixEventsSdk.UnstableValue("m.message", "org.matrix.msc1767.message"); + +/** + * An m.message event rendering + */ +exports.M_MESSAGE = M_MESSAGE; +/** + * The namespaced value for m.text + */ +const M_TEXT = new _matrixEventsSdk.UnstableValue("m.text", "org.matrix.msc1767.text"); + +/** + * The content for an m.text event + */ +exports.M_TEXT = M_TEXT; +/** + * The namespaced value for m.html + */ +const M_HTML = new _matrixEventsSdk.UnstableValue("m.html", "org.matrix.msc1767.html"); + +/** + * The content for an m.html event + */ +exports.M_HTML = M_HTML; +/** + * The namespaced value for an m.reference relation + */ +const REFERENCE_RELATION = new _matrixEventsSdk.NamespacedValue("m.reference"); + +/** + * Represents any relation type + */ +exports.REFERENCE_RELATION = REFERENCE_RELATION; +/** + * Determines if two event types are the same, including namespaces. + * @param given - The given event type. This will be compared + * against the expected type. + * @param expected - The expected event type. + * @returns True if the given type matches the expected type. + */ +function isEventTypeSame(given, expected) { + if (typeof given === "string") { + if (typeof expected === "string") { + return expected === given; + } else { + return expected.matches(given); + } + } else { + if (typeof expected === "string") { + return given.matches(expected); + } else { + const expectedNs = expected; + const givenNs = given; + return expectedNs.matches(givenNs.name) || (0, _utilities.isProvided)(givenNs.altName) && expectedNs.matches(givenNs.altName); + } + } +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/global.d.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/global.d.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/global.d.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/global.d.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,5 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true }); - require("@matrix-org/olm"); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/local_notifications.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/local_notifications.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/local_notifications.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/local_notifications.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/location.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/location.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/location.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/location.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,11 +4,8 @@ value: true }); exports.M_TIMESTAMP = exports.M_LOCATION = exports.M_ASSET = exports.LocationAssetType = void 0; - var _NamespacedValue = require("../NamespacedValue"); - var _extensible_events = require("./extensible_events"); - /* Copyright 2021 The Matrix.org Foundation C.I.C. @@ -27,19 +24,16 @@ // Types for MSC3488 - m.location: Extending events with location data let LocationAssetType; exports.LocationAssetType = LocationAssetType; - (function (LocationAssetType) { LocationAssetType["Self"] = "m.self"; LocationAssetType["Pin"] = "m.pin"; })(LocationAssetType || (exports.LocationAssetType = LocationAssetType = {})); - const M_ASSET = new _NamespacedValue.UnstableValue("m.asset", "org.matrix.msc3488.asset"); exports.M_ASSET = M_ASSET; const M_TIMESTAMP = new _NamespacedValue.UnstableValue("m.ts", "org.matrix.msc3488.ts"); /** * The event definition for an m.ts event (in content) */ - exports.M_TIMESTAMP = M_TIMESTAMP; const M_LOCATION = new _NamespacedValue.UnstableValue("m.location", "org.matrix.msc3488.location"); exports.M_LOCATION = M_LOCATION; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/partials.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/partials.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/partials.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/partials.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,7 +4,6 @@ value: true }); exports.Visibility = exports.RestrictedAllowType = exports.Preset = exports.JoinRule = exports.HistoryVisibility = exports.GuestAccess = void 0; - /* Copyright 2021 The Matrix.org Foundation C.I.C. @@ -22,25 +21,20 @@ */ let Visibility; exports.Visibility = Visibility; - (function (Visibility) { Visibility["Public"] = "public"; Visibility["Private"] = "private"; })(Visibility || (exports.Visibility = Visibility = {})); - let Preset; exports.Preset = Preset; - (function (Preset) { Preset["PrivateChat"] = "private_chat"; Preset["TrustedPrivateChat"] = "trusted_private_chat"; Preset["PublicChat"] = "public_chat"; })(Preset || (exports.Preset = Preset = {})); - // Knock and private are reserved keywords which are not yet implemented. let JoinRule; exports.JoinRule = JoinRule; - (function (JoinRule) { JoinRule["Public"] = "public"; JoinRule["Invite"] = "invite"; @@ -48,25 +42,19 @@ JoinRule["Knock"] = "knock"; JoinRule["Restricted"] = "restricted"; })(JoinRule || (exports.JoinRule = JoinRule = {})); - let RestrictedAllowType; exports.RestrictedAllowType = RestrictedAllowType; - (function (RestrictedAllowType) { RestrictedAllowType["RoomMembership"] = "m.room_membership"; })(RestrictedAllowType || (exports.RestrictedAllowType = RestrictedAllowType = {})); - let GuestAccess; exports.GuestAccess = GuestAccess; - (function (GuestAccess) { GuestAccess["CanJoin"] = "can_join"; GuestAccess["Forbidden"] = "forbidden"; })(GuestAccess || (exports.GuestAccess = GuestAccess = {})); - let HistoryVisibility; exports.HistoryVisibility = HistoryVisibility; - (function (HistoryVisibility) { HistoryVisibility["Invited"] = "invited"; HistoryVisibility["Joined"] = "joined"; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/polls.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/polls.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/polls.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/polls.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,65 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.M_POLL_START = exports.M_POLL_RESPONSE = exports.M_POLL_KIND_UNDISCLOSED = exports.M_POLL_KIND_DISCLOSED = exports.M_POLL_END = void 0; +var _matrixEventsSdk = require("matrix-events-sdk"); +/* +Copyright 2022 - 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Identifier for a disclosed poll. + */ +const M_POLL_KIND_DISCLOSED = new _matrixEventsSdk.UnstableValue("m.poll.disclosed", "org.matrix.msc3381.poll.disclosed"); + +/** + * Identifier for an undisclosed poll. + */ +exports.M_POLL_KIND_DISCLOSED = M_POLL_KIND_DISCLOSED; +const M_POLL_KIND_UNDISCLOSED = new _matrixEventsSdk.UnstableValue("m.poll.undisclosed", "org.matrix.msc3381.poll.undisclosed"); + +/** + * Any poll kind. + */ +exports.M_POLL_KIND_UNDISCLOSED = M_POLL_KIND_UNDISCLOSED; +/** + * The namespaced value for m.poll.start + */ +const M_POLL_START = new _matrixEventsSdk.UnstableValue("m.poll.start", "org.matrix.msc3381.poll.start"); + +/** + * The m.poll.start type within event content + */ +exports.M_POLL_START = M_POLL_START; +/** + * The namespaced value for m.poll.response + */ +const M_POLL_RESPONSE = new _matrixEventsSdk.UnstableValue("m.poll.response", "org.matrix.msc3381.poll.response"); + +/** + * The m.poll.response type within event content + */ +exports.M_POLL_RESPONSE = M_POLL_RESPONSE; +/** + * The namespaced value for m.poll.end + */ +const M_POLL_END = new _matrixEventsSdk.UnstableValue("m.poll.end", "org.matrix.msc3381.poll.end"); + +/** + * The event definition for an m.poll.end event (in content) + */ +exports.M_POLL_END = M_POLL_END; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/PushRules.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/PushRules.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/PushRules.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/PushRules.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,7 +5,6 @@ }); exports.TweakName = exports.RuleId = exports.PushRuleKind = exports.PushRuleActionName = exports.DMMemberCountCondition = exports.ConditionOperator = exports.ConditionKind = void 0; exports.isDmMemberCountCondition = isDmMemberCountCondition; - /* Copyright 2021 The Matrix.org Foundation C.I.C. @@ -22,28 +21,22 @@ limitations under the License. */ // allow camelcase as these are things that go onto the wire - /* eslint-disable camelcase */ let PushRuleActionName; exports.PushRuleActionName = PushRuleActionName; - (function (PushRuleActionName) { PushRuleActionName["DontNotify"] = "dont_notify"; PushRuleActionName["Notify"] = "notify"; PushRuleActionName["Coalesce"] = "coalesce"; })(PushRuleActionName || (exports.PushRuleActionName = PushRuleActionName = {})); - let TweakName; exports.TweakName = TweakName; - (function (TweakName) { TweakName["Highlight"] = "highlight"; TweakName["Sound"] = "sound"; })(TweakName || (exports.TweakName = TweakName = {})); - let ConditionOperator; exports.ConditionOperator = ConditionOperator; - (function (ConditionOperator) { ConditionOperator["ExactEquals"] = "=="; ConditionOperator["LessThan"] = "<"; @@ -51,27 +44,24 @@ ConditionOperator["GreaterThanOrEqual"] = ">="; ConditionOperator["LessThanOrEqual"] = "<="; })(ConditionOperator || (exports.ConditionOperator = ConditionOperator = {})); - const DMMemberCountCondition = "2"; exports.DMMemberCountCondition = DMMemberCountCondition; - function isDmMemberCountCondition(condition) { return condition === "==2" || condition === "2"; } - let ConditionKind; exports.ConditionKind = ConditionKind; - (function (ConditionKind) { ConditionKind["EventMatch"] = "event_match"; + ConditionKind["EventPropertyIs"] = "event_property_is"; ConditionKind["ContainsDisplayName"] = "contains_display_name"; ConditionKind["RoomMemberCount"] = "room_member_count"; ConditionKind["SenderNotificationPermission"] = "sender_notification_permission"; + ConditionKind["CallStarted"] = "call_started"; + ConditionKind["CallStartedPrefix"] = "org.matrix.msc3914.call_started"; })(ConditionKind || (exports.ConditionKind = ConditionKind = {})); - let PushRuleKind; exports.PushRuleKind = PushRuleKind; - (function (PushRuleKind) { PushRuleKind["Override"] = "override"; PushRuleKind["ContentSpecific"] = "content"; @@ -79,10 +69,8 @@ PushRuleKind["SenderSpecific"] = "sender"; PushRuleKind["Underride"] = "underride"; })(PushRuleKind || (exports.PushRuleKind = PushRuleKind = {})); - let RuleId; exports.RuleId = RuleId; - (function (RuleId) { RuleId["Master"] = ".m.rule.master"; RuleId["ContainsDisplayName"] = ".m.rule.contains_display_name"; @@ -97,5 +85,13 @@ RuleId["IncomingCall"] = ".m.rule.call"; RuleId["SuppressNotices"] = ".m.rule.suppress_notices"; RuleId["Tombstone"] = ".m.rule.tombstone"; + RuleId["PollStart"] = ".m.rule.poll_start"; + RuleId["PollStartUnstable"] = ".org.matrix.msc3930.rule.poll_start"; + RuleId["PollEnd"] = ".m.rule.poll_end"; + RuleId["PollEndUnstable"] = ".org.matrix.msc3930.rule.poll_end"; + RuleId["PollStartOneToOne"] = ".m.rule.poll_start_one_to_one"; + RuleId["PollStartOneToOneUnstable"] = ".org.matrix.msc3930.rule.poll_start_one_to_one"; + RuleId["PollEndOneToOne"] = ".m.rule.poll_end_one_to_one"; + RuleId["PollEndOneToOneUnstable"] = ".org.matrix.msc3930.rule.poll_end_one_to_one"; })(RuleId || (exports.RuleId = RuleId = {})); /* eslint-enable camelcase */ \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/read_receipts.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/read_receipts.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/read_receipts.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/read_receipts.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,8 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.ReceiptType = void 0; - +exports.ReceiptType = exports.MAIN_ROOM_TIMELINE = void 0; /* Copyright 2022 Šimon Brandner @@ -22,9 +21,10 @@ */ let ReceiptType; exports.ReceiptType = ReceiptType; - (function (ReceiptType) { ReceiptType["Read"] = "m.read"; ReceiptType["FullyRead"] = "m.fully_read"; ReceiptType["ReadPrivate"] = "m.read.private"; -})(ReceiptType || (exports.ReceiptType = ReceiptType = {})); \ No newline at end of file +})(ReceiptType || (exports.ReceiptType = ReceiptType = {})); +const MAIN_ROOM_TIMELINE = "main"; +exports.MAIN_ROOM_TIMELINE = MAIN_ROOM_TIMELINE; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/search.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/search.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/search.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/search.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,7 +4,6 @@ value: true }); exports.SearchOrderBy = void 0; - /* Copyright 2021 The Matrix.org Foundation C.I.C. @@ -21,18 +20,14 @@ limitations under the License. */ // Types relating to the /search API - /* eslint-disable camelcase */ var GroupKey; - (function (GroupKey) { GroupKey["RoomId"] = "room_id"; GroupKey["Sender"] = "sender"; })(GroupKey || (GroupKey = {})); - let SearchOrderBy; exports.SearchOrderBy = SearchOrderBy; - (function (SearchOrderBy) { SearchOrderBy["Recent"] = "recent"; SearchOrderBy["Rank"] = "rank"; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/sync.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/sync.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/sync.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/sync.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,30 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.UNREAD_THREAD_NOTIFICATIONS = void 0; +var _NamespacedValue = require("../NamespacedValue"); +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * https://github.com/matrix-org/matrix-doc/pull/3773 + * + * @experimental + */ +const UNREAD_THREAD_NOTIFICATIONS = new _NamespacedValue.ServerControlledNamespacedValue("unread_thread_notifications", "org.matrix.msc3773.unread_thread_notifications"); +exports.UNREAD_THREAD_NOTIFICATIONS = UNREAD_THREAD_NOTIFICATIONS; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/threepids.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/threepids.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/threepids.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/threepids.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,7 +4,6 @@ value: true }); exports.ThreepidMedium = void 0; - /* Copyright 2021 The Matrix.org Foundation C.I.C. @@ -21,9 +20,7 @@ limitations under the License. */ let ThreepidMedium; // TODO: Are these types universal, or specific to just /account/3pid? - exports.ThreepidMedium = ThreepidMedium; - (function (ThreepidMedium) { ThreepidMedium["Email"] = "email"; ThreepidMedium["Phone"] = "msisdn"; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/topic.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/topic.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/topic.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/topic.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,9 +4,7 @@ value: true }); exports.M_TOPIC = void 0; - var _NamespacedValue = require("../NamespacedValue"); - /* Copyright 2022 The Matrix.org Foundation C.I.C. @@ -26,10 +24,9 @@ /** * Extensible topic event type based on MSC3765 * https://github.com/matrix-org/matrix-spec-proposals/pull/3765 - */ - -/** - * Eg + * + * @example + * ``` * { * "type": "m.room.topic, * "state_key": "", @@ -44,14 +41,15 @@ * }], * } * } + * ``` */ /** * The event type for an m.topic event (in content) */ const M_TOPIC = new _NamespacedValue.UnstableValue("m.topic", "org.matrix.msc3765.topic"); + /** * The event content for an m.topic event (in content) */ - exports.M_TOPIC = M_TOPIC; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/uia.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/uia.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/@types/uia.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/@types/uia.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/utils.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/utils.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/utils.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/utils.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.DEFAULT_ALPHABET = void 0; +exports.MapWithDefault = exports.DEFAULT_ALPHABET = void 0; exports.alphabetPad = alphabetPad; exports.averageBetweenStrings = averageBetweenStrings; exports.baseToString = baseToString; @@ -19,109 +19,105 @@ exports.encodeUri = encodeUri; exports.ensureNoTrailingSlash = ensureNoTrailingSlash; exports.escapeRegExp = escapeRegExp; -exports.getCrypto = getCrypto; exports.globToRegexp = globToRegexp; +exports.immediate = immediate; exports.internaliseString = internaliseString; exports.isFunction = isFunction; exports.isNullOrUndefined = isNullOrUndefined; exports.isNumber = isNumber; exports.isSupportedReceiptType = isSupportedReceiptType; exports.lexicographicCompare = lexicographicCompare; +exports.mapsEqual = mapsEqual; exports.nextString = nextString; +exports.noUnsafeEventProps = noUnsafeEventProps; exports.normalize = normalize; exports.prevString = prevString; exports.promiseMapSeries = promiseMapSeries; exports.promiseTry = promiseTry; +exports.recursiveMapToObject = recursiveMapToObject; exports.recursivelyAssign = recursivelyAssign; exports.removeDirectionOverrideChars = removeDirectionOverrideChars; exports.removeElement = removeElement; exports.removeHiddenChars = removeHiddenChars; -exports.setCrypto = setCrypto; +exports.replaceParam = replaceParam; +exports.safeSet = safeSet; exports.simpleRetryOperation = simpleRetryOperation; exports.sleep = sleep; exports.sortEventsByLatestContentTimestamp = sortEventsByLatestContentTimestamp; exports.stringToBase = stringToBase; - +exports.unsafeProp = unsafeProp; var _unhomoglyph = _interopRequireDefault(require("unhomoglyph")); - var _pRetry = _interopRequireDefault(require("p-retry")); - var _location = require("./@types/location"); - var _read_receipts = require("./@types/read_receipts"); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/** - * This is an internal module. - * @module utils - */ +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const interns = new Map(); + /** * Internalises a string, reusing a known pointer or storing the pointer * if needed for future strings. - * @param str The string to internalise. + * @param str - The string to internalise. * @returns The internalised string. */ - function internaliseString(str) { // Unwrap strings before entering the map, if we somehow got a wrapped // string as our input. This should only happen from tests. if (str instanceof String) { str = str.toString(); - } // Check the map to see if we can store the value - + } + // Check the map to see if we can store the value if (!interns.has(str)) { interns.set(str, str); - } // Return any cached string reference - + } + // Return any cached string reference return interns.get(str); } + /** * Encode a dictionary of query parameters. * Omits any undefined/null values. - * @param {Object} params A dict of key/values to encode e.g. - * {"foo": "bar", "baz": "taz"} - * @return {string} The encoded string e.g. foo=bar&baz=taz + * @param params - A dict of key/values to encode e.g. + * `{"foo": "bar", "baz": "taz"}` + * @returns The encoded string e.g. foo=bar&baz=taz */ - - -function encodeParams(params) { - const searchParams = new URLSearchParams(); - +function encodeParams(params, urlSearchParams) { + const searchParams = urlSearchParams ?? new URLSearchParams(); for (const [key, val] of Object.entries(params)) { if (val !== undefined && val !== null) { - searchParams.set(key, String(val)); + if (Array.isArray(val)) { + val.forEach(v => { + searchParams.append(key, String(v)); + }); + } else { + searchParams.append(key, String(val)); + } } } - - return searchParams.toString(); + return searchParams; +} +/** + * Replace a stable parameter with the unstable naming for params + */ +function replaceParam(stable, unstable, dict) { + const result = _objectSpread(_objectSpread({}, dict), {}, { + [unstable]: dict[stable] + }); + delete result[stable]; + return result; } /** * Decode a query string in `application/x-www-form-urlencoded` format. - * @param {string} query A query string to decode e.g. + * @param query - A query string to decode e.g. * foo=bar&via=server1&server2 - * @return {Object} The decoded object, if any keys occurred multiple times + * @returns The decoded object, if any keys occurred multiple times * then the value will be an array of strings, else it will be an array. * This behaviour matches Node's qs.parse but is built on URLSearchParams * for native web compatibility @@ -129,50 +125,47 @@ function decodeParams(query) { const o = {}; const params = new URLSearchParams(query); - for (const key of params.keys()) { const val = params.getAll(key); o[key] = val.length === 1 ? val[0] : val; } - return o; } + /** * Encodes a URI according to a set of template variables. Variables will be * passed through encodeURIComponent. - * @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'. - * @param {Object} variables The key/value pairs to replace the template - * variables with. E.g. { "$bar": "baz" }. - * @return {string} The result of replacing all template variables e.g. '/foo/baz'. + * @param pathTemplate - The path with template variables e.g. '/foo/$bar'. + * @param variables - The key/value pairs to replace the template + * variables with. E.g. `{ "$bar": "baz" }`. + * @returns The result of replacing all template variables e.g. '/foo/baz'. */ - - function encodeUri(pathTemplate, variables) { for (const key in variables) { if (!variables.hasOwnProperty(key)) { continue; } - - pathTemplate = pathTemplate.replace(key, encodeURIComponent(variables[key])); + const value = variables[key]; + if (value === undefined || value === null) { + continue; + } + pathTemplate = pathTemplate.replace(key, encodeURIComponent(value)); } - return pathTemplate; } + /** * The removeElement() method removes the first element in the array that * satisfies (returns true) the provided testing function. - * @param {Array} array The array. - * @param {Function} fn Function to execute on each value in the array, with the - * function signature fn(element, index, array). Return true to + * @param array - The array. + * @param fn - Function to execute on each value in the array, with the + * function signature `fn(element, index, array)`. Return true to * remove this element and break. - * @param {boolean} reverse True to search in reverse order. - * @return {boolean} True if an element was removed. + * @param reverse - True to search in reverse order. + * @returns True if an element was removed. */ - - function removeElement(array, fn, reverse) { let i; - if (reverse) { for (i = array.length - 1; i >= 0; i--) { if (fn(array[i], i, array)) { @@ -188,101 +181,95 @@ } } } - return false; } + /** * Checks if the given thing is a function. - * @param {*} value The thing to check. - * @return {boolean} True if it is a function. + * @param value - The thing to check. + * @returns True if it is a function. */ - - function isFunction(value) { return Object.prototype.toString.call(value) === "[object Function]"; } + /** * Checks that the given object has the specified keys. - * @param {Object} obj The object to check. - * @param {string[]} keys The list of keys that 'obj' must have. + * @param obj - The object to check. + * @param keys - The list of keys that 'obj' must have. * @throws If the object is missing keys. */ // note using 'keys' here would shadow the 'keys' function defined above - - function checkObjectHasKeys(obj, keys) { - for (let i = 0; i < keys.length; i++) { - if (!obj.hasOwnProperty(keys[i])) { - throw new Error("Missing required key: " + keys[i]); + for (const key of keys) { + if (!obj.hasOwnProperty(key)) { + throw new Error("Missing required key: " + key); } } } + /** * Deep copy the given object. The object MUST NOT have circular references and * MUST NOT have functions. - * @param {Object} obj The object to deep copy. - * @return {Object} A copy of the object without any references to the original. + * @param obj - The object to deep copy. + * @returns A copy of the object without any references to the original. */ - - function deepCopy(obj) { return JSON.parse(JSON.stringify(obj)); } + /** * Compare two objects for equality. The objects MUST NOT have circular references. * - * @param {Object} x The first object to compare. - * @param {Object} y The second object to compare. + * @param x - The first object to compare. + * @param y - The second object to compare. * - * @return {boolean} true if the two objects are equal + * @returns true if the two objects are equal */ - - function deepCompare(x, y) { // Inspired by // http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249 + // Compare primitives and functions. // Also check if both arguments link to the same object. if (x === y) { return true; } - if (typeof x !== typeof y) { return false; - } // special-case NaN (since NaN !== NaN) - + } - if (typeof x === 'number' && isNaN(x) && isNaN(y)) { + // special-case NaN (since NaN !== NaN) + if (typeof x === "number" && isNaN(x) && isNaN(y)) { return true; - } // special-case null (since typeof null == 'object', but null.constructor - // throws) - + } + // special-case null (since typeof null == 'object', but null.constructor + // throws) if (x === null || y === null) { return x === y; - } // everything else is either an unequal primitive, or an object - + } + // everything else is either an unequal primitive, or an object if (!(x instanceof Object)) { return false; - } // check they are the same type of object - + } + // check they are the same type of object if (x.constructor !== y.constructor || x.prototype !== y.prototype) { return false; - } // special-casing for some special types of object - + } + // special-casing for some special types of object if (x instanceof RegExp || x instanceof Date) { return x.toString() === y.toString(); - } // the object algorithm works for Array, but it's sub-optimal. - + } - if (x instanceof Array) { + // the object algorithm works for Array, but it's sub-optimal. + if (Array.isArray(x)) { if (x.length !== y.length) { return false; } - for (let i = 0; i < x.length; i++) { if (!deepCompare(x[i], y[i])) { return false; @@ -294,91 +281,86 @@ if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { return false; } - } // finally, compare each of x's keys with y - + } + // finally, compare each of x's keys with y for (const p in x) { if (y.hasOwnProperty(p) !== x.hasOwnProperty(p) || !deepCompare(x[p], y[p])) { return false; } } } - return true; -} // Dev note: This returns an array of tuples, but jsdoc doesn't like that. https://github.com/jsdoc/jsdoc/issues/1703 +} +// Dev note: This returns an array of tuples, but jsdoc doesn't like that. https://github.com/jsdoc/jsdoc/issues/1703 /** * Creates an array of object properties/values (entries) then * sorts the result by key, recursively. The input object must * ensure it does not have loops. If the input is not an object * then it will be returned as-is. - * @param {*} obj The object to get entries of - * @returns {Array} The entries, sorted by key. + * @param obj - The object to get entries of + * @returns The entries, sorted by key. */ - - function deepSortedObjectEntries(obj) { - if (typeof obj !== "object") return obj; // Apparently these are object types... + if (typeof obj !== "object") return obj; + // Apparently these are object types... if (obj === null || obj === undefined || Array.isArray(obj)) return obj; const pairs = []; - for (const [k, v] of Object.entries(obj)) { pairs.push([k, deepSortedObjectEntries(v)]); - } // lexicographicCompare is faster than localeCompare, so let's use that. - + } + // lexicographicCompare is faster than localeCompare, so let's use that. pairs.sort((a, b) => lexicographicCompare(a[0], b[0])); return pairs; } + /** * Returns whether the given value is a finite number without type-coercion * - * @param {*} value the value to test - * @return {boolean} whether or not value is a finite number without type-coercion + * @param value - the value to test + * @returns whether or not value is a finite number without type-coercion */ - - function isNumber(value) { - return typeof value === 'number' && isFinite(value); + return typeof value === "number" && isFinite(value); } + /** * Removes zero width chars, diacritics and whitespace from the string * Also applies an unhomoglyph on the string, to prevent similar looking chars - * @param {string} str the string to remove hidden characters from - * @return {string} a string with the hidden characters removed + * @param str - the string to remove hidden characters from + * @returns a string with the hidden characters removed */ - - function removeHiddenChars(str) { if (typeof str === "string") { - return (0, _unhomoglyph.default)(str.normalize('NFD').replace(removeHiddenCharsRegex, '')); + return (0, _unhomoglyph.default)(str.normalize("NFD").replace(removeHiddenCharsRegex, "")); } - return ""; } + /** * Removes the direction override characters from a string - * @param {string} input * @returns string with chars removed */ - - function removeDirectionOverrideChars(str) { if (typeof str === "string") { - return str.replace(/[\u202d-\u202e]/g, ''); + return str.replace(/[\u202d-\u202e]/g, ""); } - return ""; } - function normalize(str) { // Note: we have to match the filter with the removeHiddenChars() because the // function strips spaces and other characters (M becomes RN for example, in lowercase). - return removeHiddenChars(str.toLowerCase()) // Strip all punctuation - .replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "") // We also doubly convert to lowercase to work around oddities of the library. + return removeHiddenChars(str.toLowerCase()) + // Strip all punctuation + .replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "") + // We also doubly convert to lowercase to work around oddities of the library. .toLowerCase(); -} // Regex matching bunch of unicode control characters and otherwise misleading/invisible characters. +} + +// Regex matching bunch of unicode control characters and otherwise misleading/invisible characters. // Includes: // various width spaces U+2000 - U+200D // LTR and RTL marks U+200E and U+200F @@ -386,44 +368,51 @@ // Arabic Letter RTL mark U+061C // Combining characters U+0300 - U+036F // Zero width no-break space (BOM) U+FEFF +// Blank/invisible characters (U2800, U2062-U2063) // eslint-disable-next-line no-misleading-character-class - - -const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036F\uFEFF\u061C\s]/g; - +const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036F\uFEFF\u061C\u2800\u2062-\u2063\s]/g; function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } - function globToRegexp(glob, extended = false) { // From // https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132 // Because micromatch is about 130KB with dependencies, // and minimatch is not much better. - const replacements = [[/\\\*/g, '.*'], [/\?/g, '.'], !extended && [/\\\[(!|)(.*)\\]/g, (_match, neg, pat) => ['[', neg ? '^' : '', pat.replace(/\\-/, '-'), ']'].join('')]]; - return replacements.reduce( // https://github.com/microsoft/TypeScript/issues/30134 + const replacements = [[/\\\*/g, ".*"], [/\?/g, "."]]; + if (!extended) { + replacements.push([/\\\[(!|)(.*)\\]/g, (_match, neg, pat) => ["[", neg ? "^" : "", pat.replace(/\\-/, "-"), "]"].join("")]); + } + return replacements.reduce( + // https://github.com/microsoft/TypeScript/issues/30134 (pat, args) => args ? pat.replace(args[0], args[1]) : pat, escapeRegExp(glob)); } - function ensureNoTrailingSlash(url) { - if (url && url.endsWith("/")) { + if (url?.endsWith("/")) { return url.slice(0, -1); } else { return url; } -} // Returns a promise which resolves with a given value after the given number of ms - +} +/** + * Returns a promise which resolves with a given value after the given number of ms + */ function sleep(ms, value) { return new Promise(resolve => { setTimeout(resolve, ms, value); }); } +/** + * Promise/async version of {@link setImmediate}. + */ +function immediate() { + return new Promise(setImmediate); +} function isNullOrUndefined(val) { return val === null || val === undefined; } - // Returns a Deferred function defer() { let resolve; @@ -438,39 +427,34 @@ promise }; } - -async function promiseMapSeries(promises, fn // if async/promise we don't care about the type as we only await resolution +async function promiseMapSeries(promises, fn // if async we don't care about the type as we only await resolution ) { for (const o of promises) { await fn(await o); } } - function promiseTry(fn) { return Promise.resolve(fn()); -} // Creates and awaits all promises, running no more than `chunkSize` at the same time - +} +// Creates and awaits all promises, running no more than `chunkSize` at the same time async function chunkPromises(fns, chunkSize) { const results = []; - for (let i = 0; i < fns.length; i += chunkSize) { results.push(...(await Promise.all(fns.slice(i, i + chunkSize).map(fn => fn())))); } - return results; } + /** * Retries the function until it succeeds or is interrupted. The given function must return * a promise which throws/rejects on error, otherwise the retry will assume the request * succeeded. The promise chain returned will contain the successful promise. The given function * should always return a new promise. - * @param {Function} promiseFn The function to call to get a fresh promise instance. Takes an + * @param promiseFn - The function to call to get a fresh promise instance. Takes an * attempt count as an argument, for logging/debugging purposes. - * @returns {Promise} The promise for the retried operation. + * @returns The promise for the retried operation. */ - - function simpleRetryOperation(promiseFn) { return (0, _pRetry.default)(attempt => { return promiseFn(attempt); @@ -480,23 +464,10 @@ minTimeout: 3000, // ms maxTimeout: 15000 // ms - }); -} // We need to be able to access the Node.js crypto library from within the -// Matrix SDK without needing to `require("crypto")`, which will fail in -// browsers. So `index.ts` will call `setCrypto` to store it, and when we need -// it, we can call `getCrypto`. - - -let crypto; - -function setCrypto(c) { - crypto = c; } -function getCrypto() { - return crypto; -} // String averaging inspired by https://stackoverflow.com/a/2510816 +// String averaging inspired by https://stackoverflow.com/a/2510816 // Dev note: We make the alphabet a string because it's easier to write syntactically // than arrays. Thankfully, strings implement the useful parts of the Array interface // anyhow. @@ -505,81 +476,73 @@ * The default alphabet used by string averaging in this SDK. This matches * all usefully printable ASCII characters (0x20-0x7E, inclusive). */ - - const DEFAULT_ALPHABET = (() => { let str = ""; - - for (let c = 0x20; c <= 0x7E; c++) { + for (let c = 0x20; c <= 0x7e; c++) { str += String.fromCharCode(c); } - return str; })(); + /** * Pads a string using the given alphabet as a base. The returned string will be * padded at the end with the first character in the alphabet. * * This is intended for use with string averaging. - * @param {string} s The string to pad. - * @param {number} n The length to pad to. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The padded string. + * @param s - The string to pad. + * @param n - The length to pad to. + * @param alphabet - The alphabet to use as a single string. + * @returns The padded string. */ - - exports.DEFAULT_ALPHABET = DEFAULT_ALPHABET; - function alphabetPad(s, n, alphabet = DEFAULT_ALPHABET) { return s.padEnd(n, alphabet[0]); } + /** * Converts a baseN number to a string, where N is the alphabet's length. * * This is intended for use with string averaging. - * @param {bigint} n The baseN number. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The baseN number encoded as a string from the alphabet. + * @param n - The baseN number. + * @param alphabet - The alphabet to use as a single string. + * @returns The baseN number encoded as a string from the alphabet. */ - - function baseToString(n, alphabet = DEFAULT_ALPHABET) { // Developer note: the stringToBase() function offsets the character set by 1 so that repeated // characters (ie: "aaaaaa" in a..z) don't come out as zero. We have to reverse this here as // otherwise we'll be wrong in our conversion. Undoing a +1 before an exponent isn't very fun // though, so we rely on a lengthy amount of `x - 1` and integer division rules to reach a // sane state. This also means we have to do rollover detection: see below. - const len = BigInt(alphabet.length); + const len = BigInt(alphabet.length); if (n <= len) { return alphabet[Number(n) - 1] ?? ""; } - let d = n / len; - let r = Number(n % len) - 1; // Rollover detection: if the remainder is negative, it means that the string needs + let r = Number(n % len) - 1; + + // Rollover detection: if the remainder is negative, it means that the string needs // to roll over by 1 character downwards (ie: in a..z, the previous to "aaa" would be // "zz"). - if (r < 0) { d -= BigInt(Math.abs(r)); // abs() is just to be clear what we're doing. Could also `+= r`. - r = Number(len) - 1; } - return baseToString(d, alphabet) + alphabet[r]; } + /** * Converts a string to a baseN number, where N is the alphabet's length. * * This is intended for use with string averaging. - * @param {string} s The string to convert to a number. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {bigint} The baseN number. + * @param s - The string to convert to a number. + * @param alphabet - The alphabet to use as a single string. + * @returns The baseN number. */ - - function stringToBase(s, alphabet = DEFAULT_ALPHABET) { - const len = BigInt(alphabet.length); // In our conversion to baseN we do a couple performance optimizations to avoid using + const len = BigInt(alphabet.length); + + // In our conversion to baseN we do a couple performance optimizations to avoid using // excess CPU and such. To create baseN numbers, the input string needs to be reversed // so the exponents stack up appropriately, as the last character in the unreversed // string has less impact than the first character (in "abc" the A is a lot more important @@ -587,79 +550,75 @@ // alphabet lookup, avoiding an index scan of `alphabet.indexOf(reversedStr[i])` - we know // that the alphabet and (theoretically) the input string are constrained on character sets // and thus can do simple subtraction to end up with the same result. + // Developer caution: we carefully cast to BigInt here to avoid losing precision. We cannot // rely on Math.pow() (for example) to be capable of handling our insane numbers. let result = BigInt(0); - for (let i = s.length - 1, j = BigInt(0); i >= 0; i--, j++) { - const charIndex = s.charCodeAt(i) - alphabet.charCodeAt(0); // We add 1 to the char index to offset the whole numbering scheme. We unpack this in - // the baseToString() function. + const charIndex = s.charCodeAt(i) - alphabet.charCodeAt(0); + // We add 1 to the char index to offset the whole numbering scheme. We unpack this in + // the baseToString() function. result += BigInt(1 + charIndex) * len ** j; } - return result; } + /** * Averages two strings, returning the midpoint between them. This is accomplished by * converting both to baseN numbers (where N is the alphabet's length) then averaging * those before re-encoding as a string. - * @param {string} a The first string. - * @param {string} b The second string. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The midpoint between the strings, as a string. + * @param a - The first string. + * @param b - The second string. + * @param alphabet - The alphabet to use as a single string. + * @returns The midpoint between the strings, as a string. */ - - function averageBetweenStrings(a, b, alphabet = DEFAULT_ALPHABET) { const padN = Math.max(a.length, b.length); const baseA = stringToBase(alphabetPad(a, padN, alphabet), alphabet); const baseB = stringToBase(alphabetPad(b, padN, alphabet), alphabet); - const avg = (baseA + baseB) / BigInt(2); // Detect integer division conflicts. This happens when two numbers are divided too close so - // we lose a .5 precision. We need to add a padding character in these cases. + const avg = (baseA + baseB) / BigInt(2); + // Detect integer division conflicts. This happens when two numbers are divided too close so + // we lose a .5 precision. We need to add a padding character in these cases. if (avg === baseA || avg == baseB) { return baseToString(avg, alphabet) + alphabet[0]; } - return baseToString(avg, alphabet); } + /** * Finds the next string using the alphabet provided. This is done by converting the * string to a baseN number, where N is the alphabet's length, then adding 1 before * converting back to a string. - * @param {string} s The string to start at. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The string which follows the input string. + * @param s - The string to start at. + * @param alphabet - The alphabet to use as a single string. + * @returns The string which follows the input string. */ - - function nextString(s, alphabet = DEFAULT_ALPHABET) { return baseToString(stringToBase(s, alphabet) + BigInt(1), alphabet); } + /** * Finds the previous string using the alphabet provided. This is done by converting the * string to a baseN number, where N is the alphabet's length, then subtracting 1 before * converting back to a string. - * @param {string} s The string to start at. - * @param {string} alphabet The alphabet to use as a single string. - * @returns {string} The string which precedes the input string. + * @param s - The string to start at. + * @param alphabet - The alphabet to use as a single string. + * @returns The string which precedes the input string. */ - - function prevString(s, alphabet = DEFAULT_ALPHABET) { return baseToString(stringToBase(s, alphabet) - BigInt(1), alphabet); } + /** * Compares strings lexicographically as a sort-safe function. - * @param {string} a The first (reference) string. - * @param {string} b The second (compare) string. - * @returns {number} Negative if the reference string is before the compare string; + * @param a - The first (reference) string. + * @param b - The second (compare) string. + * @returns Negative if the reference string is before the compare string; * positive if the reference string is after; and zero if equal. */ - - function lexicographicCompare(a, b) { // Dev note: this exists because I'm sad that you can use math operators on strings, so I've // hidden the operation in this function. @@ -671,56 +630,112 @@ return 0; } } - const collator = new Intl.Collator(); /** * Performant language-sensitive string comparison - * @param a the first string to compare - * @param b the second string to compare + * @param a - the first string to compare + * @param b - the second string to compare */ - function compare(a, b) { return collator.compare(a, b); } + /** * This function is similar to Object.assign() but it assigns recursively and * allows you to ignore nullish values from the source * - * @param {Object} target - * @param {Object} source * @returns the target object */ - - function recursivelyAssign(target, source, ignoreNullish = false) { for (const [sourceKey, sourceValue] of Object.entries(source)) { if (target[sourceKey] instanceof Object && sourceValue) { recursivelyAssign(target[sourceKey], sourceValue); continue; } - if (sourceValue !== null && sourceValue !== undefined || !ignoreNullish) { target[sourceKey] = sourceValue; continue; } } - return target; } - function getContentTimestampWithFallback(event) { return _location.M_TIMESTAMP.findIn(event.getContent()) ?? -1; } + /** * Sort events by their content m.ts property * Latest timestamp first */ - - function sortEventsByLatestContentTimestamp(left, right) { return getContentTimestampWithFallback(right) - getContentTimestampWithFallback(left); } - function isSupportedReceiptType(receiptType) { return [_read_receipts.ReceiptType.Read, _read_receipts.ReceiptType.ReadPrivate].includes(receiptType); -} \ No newline at end of file +} + +/** + * Determines whether two maps are equal. + * @param eq - The equivalence relation to compare values by. Defaults to strict equality. + */ +function mapsEqual(x, y, eq = (v1, v2) => v1 === v2) { + if (x.size !== y.size) return false; + for (const [k, v1] of x) { + const v2 = y.get(k); + if (v2 === undefined || !eq(v1, v2)) return false; + } + return true; +} +function processMapToObjectValue(value) { + if (value instanceof Map) { + // Value is a Map. Recursively map it to an object. + return recursiveMapToObject(value); + } else if (Array.isArray(value)) { + // Value is an Array. Recursively map the value (e.g. to cover Array of Arrays). + return value.map(v => processMapToObjectValue(v)); + } else { + return value; + } +} + +/** + * Recursively converts Maps to plain objects. + * Also supports sub-lists of Maps. + */ +function recursiveMapToObject(map) { + const targetMap = new Map(); + for (const [key, value] of map) { + targetMap.set(key, processMapToObjectValue(value)); + } + return Object.fromEntries(targetMap.entries()); +} +function unsafeProp(prop) { + return prop === "__proto__" || prop === "prototype" || prop === "constructor"; +} +function safeSet(obj, prop, value) { + if (unsafeProp(prop)) { + throw new Error("Trying to modify prototype or constructor"); + } + obj[prop] = value; +} +function noUnsafeEventProps(event) { + return !(unsafeProp(event.room_id) || unsafeProp(event.sender) || unsafeProp(event.user_id) || unsafeProp(event.event_id)); +} +class MapWithDefault extends Map { + constructor(createDefault) { + super(); + this.createDefault = createDefault; + } + + /** + * Returns the value if the key already exists. + * If not, it creates a new value under that key using the ctor callback and returns it. + */ + getOrCreate(key) { + if (!this.has(key)) { + this.set(key, this.createDefault()); + } + return this.get(key); + } +} +exports.MapWithDefault = MapWithDefault; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/audioContext.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/audioContext.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/audioContext.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/audioContext.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,52 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.releaseContext = exports.acquireContext = void 0; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +let audioContext = null; +let refCount = 0; + +/** + * Acquires a reference to the shared AudioContext. + * It's highly recommended to reuse this AudioContext rather than creating your + * own, because multiple AudioContexts can be problematic in some browsers. + * Make sure to call releaseContext when you're done using it. + * @returns The shared AudioContext + */ +const acquireContext = () => { + if (audioContext === null) audioContext = new AudioContext(); + refCount++; + return audioContext; +}; + +/** + * Signals that one of the references to the shared AudioContext has been + * released, allowing the context and associated hardware resources to be + * cleaned up if nothing else is using it. + */ +exports.acquireContext = acquireContext; +const releaseContext = () => { + refCount--; + if (refCount === 0) { + audioContext?.close(); + audioContext = null; + } +}; +exports.releaseContext = releaseContext; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callEventHandler.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callEventHandler.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callEventHandler.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callEventHandler.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,107 +4,90 @@ value: true }); exports.CallEventHandlerEvent = exports.CallEventHandler = void 0; - -var _event = require("../models/event"); - var _logger = require("../logger"); - var _call = require("./call"); - -var _event2 = require("../@types/event"); - +var _event = require("../@types/event"); var _client = require("../client"); - -var _sync = require("../sync"); - +var _groupCall = require("./groupCall"); var _room = require("../models/room"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } // Don't ring unless we'd be ringing for at least 3 seconds: the user needs some // time to press the 'accept' button const RING_GRACE_PERIOD = 3000; let CallEventHandlerEvent; exports.CallEventHandlerEvent = CallEventHandlerEvent; - (function (CallEventHandlerEvent) { CallEventHandlerEvent["Incoming"] = "Call.incoming"; })(CallEventHandlerEvent || (exports.CallEventHandlerEvent = CallEventHandlerEvent = {})); - class CallEventHandler { - constructor(client) { - _defineProperty(this, "client", void 0); + // XXX: Most of these are only public because of the tests + constructor(client) { _defineProperty(this, "calls", void 0); - _defineProperty(this, "callEventBuffer", void 0); - + _defineProperty(this, "nextSeqByCall", new Map()); + _defineProperty(this, "toDeviceEventBuffers", new Map()); + _defineProperty(this, "client", void 0); _defineProperty(this, "candidateEventsByCall", void 0); - - _defineProperty(this, "evaluateEventBuffer", async () => { - if (this.client.getSyncState() === _sync.SyncState.Syncing) { - await Promise.all(this.callEventBuffer.map(event => { - this.client.decryptEventIfNeeded(event); - })); - const ignoreCallIds = new Set(); // inspect the buffer and mark all calls which have been answered - // or hung up before passing them to the call event handler. - - for (const ev of this.callEventBuffer) { - if (ev.getType() === _event2.EventType.CallAnswer || ev.getType() === _event2.EventType.CallHangup) { - ignoreCallIds.add(ev.getContent().call_id); - } - } // now loop through the buffer chronologically and inject them - - - for (const e of this.callEventBuffer) { - if (e.getType() === _event2.EventType.CallInvite && ignoreCallIds.has(e.getContent().call_id)) { - // This call has previously been answered or hung up: ignore it - continue; - } - - try { - await this.handleCallEvent(e); - } catch (e) { - _logger.logger.error("Caught exception handling call event", e); - } - } - - this.callEventBuffer = []; + _defineProperty(this, "eventBufferPromiseChain", void 0); + _defineProperty(this, "onSync", () => { + // Process the current event buffer and start queuing into a new one. + const currentEventBuffer = this.callEventBuffer; + this.callEventBuffer = []; + + // Ensure correct ordering by only processing this queue after the previous one has finished processing + if (this.eventBufferPromiseChain) { + this.eventBufferPromiseChain = this.eventBufferPromiseChain.then(() => this.evaluateEventBuffer(currentEventBuffer)); + } else { + this.eventBufferPromiseChain = this.evaluateEventBuffer(currentEventBuffer); } }); - _defineProperty(this, "onRoomTimeline", event => { - this.client.decryptEventIfNeeded(event); // any call events or ones that might be once they're decrypted - - if (this.eventIsACall(event) || event.isBeingDecrypted()) { - // queue up for processing once all events from this sync have been - // processed (see above). + this.callEventBuffer.push(event); + }); + _defineProperty(this, "onToDeviceEvent", event => { + const content = event.getContent(); + if (!content.call_id) { this.callEventBuffer.push(event); + return; } - - if (event.isBeingDecrypted() || event.isDecryptionFailure()) { - // add an event listener for once the event is decrypted. - event.once(_event.MatrixEventEvent.Decrypted, async () => { - if (!this.eventIsACall(event)) return; - - if (this.callEventBuffer.includes(event)) { - // we were waiting for that event to decrypt, so recheck the buffer - this.evaluateEventBuffer(); - } else { - // This one wasn't buffered so just run the event handler for it - // straight away - try { - await this.handleCallEvent(event); - } catch (e) { - _logger.logger.error("Caught exception handling call event", e); - } - } - }); + if (!this.nextSeqByCall.has(content.call_id)) { + this.nextSeqByCall.set(content.call_id, 0); + } + if (content.seq === undefined) { + this.callEventBuffer.push(event); + return; + } + const nextSeq = this.nextSeqByCall.get(content.call_id) || 0; + if (content.seq !== nextSeq) { + if (!this.toDeviceEventBuffers.has(content.call_id)) { + this.toDeviceEventBuffers.set(content.call_id, []); + } + const buffer = this.toDeviceEventBuffers.get(content.call_id); + const index = buffer.findIndex(e => e.getContent().seq > content.seq); + if (index === -1) { + buffer.push(event); + } else { + buffer.splice(index, 0, event); + } + } else { + const callId = content.call_id; + this.callEventBuffer.push(event); + this.nextSeqByCall.set(callId, content.seq + 1); + const buffer = this.toDeviceEventBuffers.get(callId); + let nextEvent = buffer && buffer.shift(); + while (nextEvent && nextEvent.getContent().seq === this.nextSeqByCall.get(callId)) { + this.callEventBuffer.push(nextEvent); + this.nextSeqByCall.set(callId, nextEvent.getContent().seq + 1); + nextEvent = buffer.shift(); + } } }); - this.client = client; - this.calls = new Map(); // The sync code always emits one event at a time, so it will patiently + this.calls = new Map(); + // The sync code always emits one event at a time, so it will patiently // wait for us to finish processing a call invite before delivering the // next event, even if that next event is a hangup. We therefore accumulate // all our call events and then process them on the 'sync' event, ie. @@ -112,132 +95,174 @@ // call events if we get both the invite and answer/hangup in the same sync. // This happens quite often, eg. replaying sync from storage, catchup sync // after loading and after we've been offline for a bit. - this.callEventBuffer = []; this.candidateEventsByCall = new Map(); } - start() { - this.client.on(_client.ClientEvent.Sync, this.evaluateEventBuffer); + this.client.on(_client.ClientEvent.Sync, this.onSync); this.client.on(_room.RoomEvent.Timeline, this.onRoomTimeline); + this.client.on(_client.ClientEvent.ToDeviceEvent, this.onToDeviceEvent); } - stop() { - this.client.removeListener(_client.ClientEvent.Sync, this.evaluateEventBuffer); + this.client.removeListener(_client.ClientEvent.Sync, this.onSync); this.client.removeListener(_room.RoomEvent.Timeline, this.onRoomTimeline); + this.client.removeListener(_client.ClientEvent.ToDeviceEvent, this.onToDeviceEvent); } + async evaluateEventBuffer(eventBuffer) { + await Promise.all(eventBuffer.map(event => this.client.decryptEventIfNeeded(event))); + const callEvents = eventBuffer.filter(event => { + const eventType = event.getType(); + return eventType.startsWith("m.call.") || eventType.startsWith("org.matrix.call."); + }); + const ignoreCallIds = new Set(); - eventIsACall(event) { - const type = event.getType(); - /** - * Unstable prefixes: - * - org.matrix.call. : MSC3086 https://github.com/matrix-org/matrix-doc/pull/3086 - */ + // inspect the buffer and mark all calls which have been answered + // or hung up before passing them to the call event handler. + for (const event of callEvents) { + const eventType = event.getType(); + if (eventType === _event.EventType.CallAnswer || eventType === _event.EventType.CallHangup) { + ignoreCallIds.add(event.getContent().call_id); + } + } - return type.startsWith("m.call.") || type.startsWith("org.matrix.call."); + // Process call events in the order that they were received + for (const event of callEvents) { + const eventType = event.getType(); + const callId = event.getContent().call_id; + if (eventType === _event.EventType.CallInvite && ignoreCallIds.has(callId)) { + // This call has previously been answered or hung up: ignore it + continue; + } + try { + await this.handleCallEvent(event); + } catch (e) { + _logger.logger.error("CallEventHandler evaluateEventBuffer() caught exception handling call event", e); + } + } } - async handleCallEvent(event) { + this.client.emit(_client.ClientEvent.ReceivedVoipEvent, event); const content = event.getContent(); + const callRoomId = event.getRoomId() || this.client.groupCallEventHandler.getGroupCallById(content.conf_id)?.room?.roomId; + const groupCallId = content.conf_id; const type = event.getType(); - const weSentTheEvent = event.getSender() === this.client.credentials.userId; - let call = content.call_id ? this.calls.get(content.call_id) : undefined; //console.info("RECV %s content=%s", type, JSON.stringify(content)); - - if (type === _event2.EventType.CallInvite) { + const senderId = event.getSender(); + let call = content.call_id ? this.calls.get(content.call_id) : undefined; + let opponentDeviceId; + let groupCall; + if (groupCallId) { + groupCall = this.client.groupCallEventHandler.getGroupCallById(groupCallId); + if (!groupCall) { + _logger.logger.warn(`CallEventHandler handleCallEvent() could not find a group call - ignoring event (groupCallId=${groupCallId}, type=${type})`); + return; + } + opponentDeviceId = content.device_id; + if (!opponentDeviceId) { + _logger.logger.warn(`CallEventHandler handleCallEvent() could not find a device id - ignoring event (senderId=${senderId})`); + groupCall.emit(_groupCall.GroupCallEvent.Error, new _groupCall.GroupCallUnknownDeviceError(senderId)); + return; + } + if (content.dest_session_id !== this.client.getSessionId()) { + _logger.logger.warn("CallEventHandler handleCallEvent() call event does not match current session id - ignoring"); + return; + } + } + const weSentTheEvent = senderId === this.client.credentials.userId && (opponentDeviceId === undefined || opponentDeviceId === this.client.getDeviceId()); + if (!callRoomId) return; + if (type === _event.EventType.CallInvite) { // ignore invites you send - if (weSentTheEvent) return; // expired call - - if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) return; // stale/old invite event - + if (weSentTheEvent) return; + // expired call + if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) return; + // stale/old invite event if (call && call.state === _call.CallState.Ended) return; - if (call) { - _logger.logger.log(`WARN: Already have a MatrixCall with id ${content.call_id} but got an ` + `invite. Clobbering.`); + _logger.logger.warn(`CallEventHandler handleCallEvent() already has a call but got an invite - clobbering (callId=${content.call_id})`); + } + if (content.invitee && content.invitee !== this.client.getUserId()) { + return; // This invite was meant for another user in the room } - const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now(); - - _logger.logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); - - call = (0, _call.createNewMatrixCall)(this.client, event.getRoomId(), { - forceTURN: this.client.forceTURN - }); - + const timeUntilTurnCresExpire = (this.client.getTurnServersExpiry() ?? 0) - Date.now(); + _logger.logger.info("CallEventHandler handleCallEvent() current turn creds expire in " + timeUntilTurnCresExpire + " ms"); + call = (0, _call.createNewMatrixCall)(this.client, callRoomId, { + forceTURN: this.client.forceTURN, + opponentDeviceId, + groupCallId, + opponentSessionId: content.sender_session_id + }) ?? undefined; if (!call) { - _logger.logger.log("Incoming call ID " + content.call_id + " but this client " + "doesn't support WebRTC"); // don't hang up the call: there could be other clients + _logger.logger.log(`CallEventHandler handleCallEvent() this client does not support WebRTC (callId=${content.call_id})`); + // don't hang up the call: there could be other clients // connected that do support WebRTC and declining the // the call on their behalf would be really annoying. - - return; } - call.callId = content.call_id; - await call.initWithInvite(event); - this.calls.set(call.callId, call); // if we stashed candidate events for that call ID, play them back now + try { + await call.initWithInvite(event); + } catch (e) { + if (e instanceof _call.CallError) { + if (e.code === _groupCall.GroupCallErrorCode.UnknownDevice) { + groupCall?.emit(_groupCall.GroupCallEvent.Error, e); + } else { + _logger.logger.error(e); + } + } + } + this.calls.set(call.callId, call); + // if we stashed candidate events for that call ID, play them back now if (this.candidateEventsByCall.get(call.callId)) { for (const ev of this.candidateEventsByCall.get(call.callId)) { call.onRemoteIceCandidatesReceived(ev); } - } // Were we trying to call that user (room)? - + } + // Were we trying to call that user (room)? let existingCall; - for (const thisCall of this.calls.values()) { const isCalling = [_call.CallState.WaitLocalMedia, _call.CallState.CreateOffer, _call.CallState.InviteSent].includes(thisCall.state); - - if (call.roomId === thisCall.roomId && thisCall.direction === _call.CallDirection.Outbound && isCalling) { + if (call.roomId === thisCall.roomId && thisCall.direction === _call.CallDirection.Outbound && call.getOpponentMember()?.userId === thisCall.invitee && isCalling) { existingCall = thisCall; break; } } - if (existingCall) { - // If we've only got to wait_local_media or create_offer and - // we've got an invite, pick the incoming call because we know - // we haven't sent our invite yet otherwise, pick whichever - // call has the lowest call ID (by string comparison) - if (existingCall.state === _call.CallState.WaitLocalMedia || existingCall.state === _call.CallState.CreateOffer || existingCall.callId > call.callId) { - _logger.logger.log("Glare detected: answering incoming call " + call.callId + " and canceling outgoing call " + existingCall.callId); - + if (existingCall.callId > call.callId) { + _logger.logger.log(`CallEventHandler handleCallEvent() detected glare - answering incoming call and canceling outgoing call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`); existingCall.replacedBy(call); - call.answer(); } else { - _logger.logger.log("Glare detected: rejecting incoming call " + call.callId + " and keeping outgoing call " + existingCall.callId); - + _logger.logger.log(`CallEventHandler handleCallEvent() detected glare - hanging up incoming call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`); call.hangup(_call.CallErrorCode.Replaced, true); } } else { this.client.emit(CallEventHandlerEvent.Incoming, call); } - return; - } else if (type === _event2.EventType.CallCandidates) { + } else if (type === _event.EventType.CallCandidates) { if (weSentTheEvent) return; - if (!call) { // store the candidates; we may get a call eventually. if (!this.candidateEventsByCall.has(content.call_id)) { this.candidateEventsByCall.set(content.call_id, []); } - this.candidateEventsByCall.get(content.call_id).push(event); } else { call.onRemoteIceCandidatesReceived(event); } - return; - } else if ([_event2.EventType.CallHangup, _event2.EventType.CallReject].includes(type)) { + } else if ([_event.EventType.CallHangup, _event.EventType.CallReject].includes(type)) { // Note that we also observe our own hangups here so we can see // if we've already rejected a call that would otherwise be valid if (!call) { // if not live, store the fact that the call has ended because // we're probably getting events backwards so // the hangup will come before the invite - call = (0, _call.createNewMatrixCall)(this.client, event.getRoomId()); - + call = (0, _call.createNewMatrixCall)(this.client, callRoomId, { + opponentDeviceId, + opponentSessionId: content.sender_session_id + }) ?? undefined; if (call) { call.callId = content.call_id; call.initWithHangup(event); @@ -245,34 +270,30 @@ } } else { if (call.state !== _call.CallState.Ended) { - if (type === _event2.EventType.CallHangup) { + if (type === _event.EventType.CallHangup) { call.onHangupReceived(content); } else { call.onRejectReceived(content); - } // @ts-expect-error typescript thinks the state can't be 'ended' because we're + } + + // @ts-expect-error typescript thinks the state can't be 'ended' because we're // inside the if block where it wasn't, but it could have changed because // on[Hangup|Reject]Received are side-effecty. - - if (call.state === _call.CallState.Ended) this.calls.delete(content.call_id); } } - return; - } // The following events need a call and a peer connection - + } + // The following events need a call and a peer connection if (!call || !call.hasPeerConnection) { - _logger.logger.info(`Discarding possible call event ${event.getId()} as we don't have a call/peerConn`, type); - + _logger.logger.info(`CallEventHandler handleCallEvent() discarding possible call event as we don't have a call (type=${type})`); return; - } // Ignore remote echo - - + } + // Ignore remote echo if (event.getContent().party_id === call.ourPartyId) return; - switch (type) { - case _event2.EventType.CallAnswer: + case _event.EventType.CallAnswer: if (weSentTheEvent) { if (call.state === _call.CallState.Ringing) { call.onAnsweredElsewhere(content); @@ -280,29 +301,22 @@ } else { call.onAnswerReceived(event); } - break; - - case _event2.EventType.CallSelectAnswer: + case _event.EventType.CallSelectAnswer: call.onSelectAnswerReceived(event); break; - - case _event2.EventType.CallNegotiate: + case _event.EventType.CallNegotiate: call.onNegotiateReceived(event); break; - - case _event2.EventType.CallAssertedIdentity: - case _event2.EventType.CallAssertedIdentityPrefix: + case _event.EventType.CallAssertedIdentity: + case _event.EventType.CallAssertedIdentityPrefix: call.onAssertedIdentityReceived(event); break; - - case _event2.EventType.CallSDPStreamMetadataChanged: - case _event2.EventType.CallSDPStreamMetadataChangedPrefix: + case _event.EventType.CallSDPStreamMetadataChanged: + case _event.EventType.CallSDPStreamMetadataChangedPrefix: call.onSDPStreamMetadataChangedReceived(event); break; } } - } - exports.CallEventHandler = CallEventHandler; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callEventTypes.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callEventTypes.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callEventTypes.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callEventTypes.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,14 +5,13 @@ }); exports.SDPStreamMetadataPurpose = exports.SDPStreamMetadataKey = void 0; // allow non-camelcase as these are events type that go onto the wire - /* eslint-disable camelcase */ + // TODO: Change to "sdp_stream_metadata" when MSC3077 is merged const SDPStreamMetadataKey = "org.matrix.msc3077.sdp_stream_metadata"; exports.SDPStreamMetadataKey = SDPStreamMetadataKey; let SDPStreamMetadataPurpose; exports.SDPStreamMetadataPurpose = SDPStreamMetadataPurpose; - (function (SDPStreamMetadataPurpose) { SDPStreamMetadataPurpose["Usermedia"] = "m.usermedia"; SDPStreamMetadataPurpose["Screenshare"] = "m.screenshare"; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callFeed.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callFeed.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callFeed.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/callFeed.js 2023-04-11 06:11:52.000000000 +0000 @@ -4,144 +4,139 @@ value: true }); exports.SPEAKING_THRESHOLD = exports.CallFeedEvent = exports.CallFeed = void 0; - +var _callEventTypes = require("./callEventTypes"); +var _audioContext = require("./audioContext"); +var _logger = require("../logger"); var _typedEventEmitter = require("../models/typed-event-emitter"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +var _call = require("./call"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } const POLLING_INTERVAL = 200; // ms - const SPEAKING_THRESHOLD = -60; // dB - exports.SPEAKING_THRESHOLD = SPEAKING_THRESHOLD; const SPEAKING_SAMPLE_COUNT = 8; // samples - let CallFeedEvent; exports.CallFeedEvent = CallFeedEvent; - (function (CallFeedEvent) { CallFeedEvent["NewStream"] = "new_stream"; CallFeedEvent["MuteStateChanged"] = "mute_state_changed"; + CallFeedEvent["LocalVolumeChanged"] = "local_volume_changed"; CallFeedEvent["VolumeChanged"] = "volume_changed"; + CallFeedEvent["ConnectedChanged"] = "connected_changed"; CallFeedEvent["Speaking"] = "speaking"; + CallFeedEvent["Disposed"] = "disposed"; })(CallFeedEvent || (exports.CallFeedEvent = CallFeedEvent = {})); - class CallFeed extends _typedEventEmitter.TypedEventEmitter { constructor(opts) { super(); - _defineProperty(this, "stream", void 0); - + _defineProperty(this, "sdpMetadataStreamId", void 0); _defineProperty(this, "userId", void 0); - + _defineProperty(this, "deviceId", void 0); _defineProperty(this, "purpose", void 0); - _defineProperty(this, "speakingVolumeSamples", void 0); - _defineProperty(this, "client", void 0); - + _defineProperty(this, "call", void 0); _defineProperty(this, "roomId", void 0); - _defineProperty(this, "audioMuted", void 0); - _defineProperty(this, "videoMuted", void 0); - + _defineProperty(this, "localVolume", 1); _defineProperty(this, "measuringVolumeActivity", false); - _defineProperty(this, "audioContext", void 0); - _defineProperty(this, "analyser", void 0); - _defineProperty(this, "frequencyBinCount", void 0); - _defineProperty(this, "speakingThreshold", SPEAKING_THRESHOLD); - _defineProperty(this, "speaking", false); - _defineProperty(this, "volumeLooperTimeout", void 0); - + _defineProperty(this, "_disposed", false); + _defineProperty(this, "_connected", false); _defineProperty(this, "onAddTrack", () => { this.emit(CallFeedEvent.NewStream, this.stream); }); - + _defineProperty(this, "onCallState", state => { + if (state === _call.CallState.Connected) { + this.connected = true; + } else if (state === _call.CallState.Connecting) { + this.connected = false; + } + }); _defineProperty(this, "volumeLooper", () => { if (!this.analyser) return; if (!this.measuringVolumeActivity) return; this.analyser.getFloatFrequencyData(this.frequencyBinCount); let maxVolume = -Infinity; - - for (let i = 0; i < this.frequencyBinCount.length; i++) { - if (this.frequencyBinCount[i] > maxVolume) { - maxVolume = this.frequencyBinCount[i]; + for (const volume of this.frequencyBinCount) { + if (volume > maxVolume) { + maxVolume = volume; } } - this.speakingVolumeSamples.shift(); this.speakingVolumeSamples.push(maxVolume); this.emit(CallFeedEvent.VolumeChanged, maxVolume); let newSpeaking = false; - - for (let i = 0; i < this.speakingVolumeSamples.length; i++) { - const volume = this.speakingVolumeSamples[i]; - + for (const volume of this.speakingVolumeSamples) { if (volume > this.speakingThreshold) { newSpeaking = true; break; } } - if (this.speaking !== newSpeaking) { this.speaking = newSpeaking; this.emit(CallFeedEvent.Speaking, this.speaking); } - this.volumeLooperTimeout = setTimeout(this.volumeLooper, POLLING_INTERVAL); }); - this.client = opts.client; + this.call = opts.call; this.roomId = opts.roomId; this.userId = opts.userId; + this.deviceId = opts.deviceId; this.purpose = opts.purpose; this.audioMuted = opts.audioMuted; this.videoMuted = opts.videoMuted; this.speakingVolumeSamples = new Array(SPEAKING_SAMPLE_COUNT).fill(-Infinity); + this.sdpMetadataStreamId = opts.stream.id; this.updateStream(null, opts.stream); + this.stream = opts.stream; // updateStream does this, but this makes TS happier if (this.hasAudioTrack) { this.initVolumeMeasuring(); } + if (opts.call) { + opts.call.addListener(_call.CallEvent.State, this.onCallState); + this.onCallState(opts.call.state); + } + } + get connected() { + // Local feeds are always considered connected + return this.isLocal() || this._connected; + } + set connected(connected) { + this._connected = connected; + this.emit(CallFeedEvent.ConnectedChanged, this.connected); } - get hasAudioTrack() { return this.stream.getAudioTracks().length > 0; } - updateStream(oldStream, newStream) { if (newStream === oldStream) return; - if (oldStream) { oldStream.removeEventListener("addtrack", this.onAddTrack); this.measureVolumeActivity(false); } - - if (newStream) { - this.stream = newStream; - newStream.addEventListener("addtrack", this.onAddTrack); - - if (this.hasAudioTrack) { - this.initVolumeMeasuring(); - } else { - this.measureVolumeActivity(false); - } + this.stream = newStream; + newStream.addEventListener("addtrack", this.onAddTrack); + if (this.hasAudioTrack) { + this.initVolumeMeasuring(); + } else { + this.measureVolumeActivity(false); } - this.emit(CallFeedEvent.NewStream, this.stream); } - initVolumeMeasuring() { - const AudioContext = window.AudioContext || window.webkitAudioContext; - if (!this.hasAudioTrack || !AudioContext) return; - this.audioContext = new AudioContext(); + if (!this.hasAudioTrack) return; + if (!this.audioContext) this.audioContext = (0, _audioContext.acquireContext)(); this.analyser = this.audioContext.createAnalyser(); this.analyser.fftSize = 512; this.analyser.smoothingTimeConstant = 0.1; @@ -149,77 +144,80 @@ mediaStreamAudioSourceNode.connect(this.analyser); this.frequencyBinCount = new Float32Array(this.analyser.frequencyBinCount); } - /** * Returns callRoom member * @returns member of the callRoom */ getMember() { const callRoom = this.client.getRoom(this.roomId); - return callRoom.getMember(this.userId); + return callRoom?.getMember(this.userId) ?? null; } + /** * Returns true if CallFeed is local, otherwise returns false - * @returns {boolean} is local? + * @returns is local? */ - - isLocal() { - return this.userId === this.client.getUserId(); + return this.userId === this.client.getUserId() && (this.deviceId === undefined || this.deviceId === this.client.getDeviceId()); } + /** * Returns true if audio is muted or if there are no audio * tracks, otherwise returns false - * @returns {boolean} is audio muted? + * @returns is audio muted? */ - - isAudioMuted() { return this.stream.getAudioTracks().length === 0 || this.audioMuted; } + /** * Returns true video is muted or if there are no video * tracks, otherwise returns false - * @returns {boolean} is video muted? + * @returns is video muted? */ - - isVideoMuted() { // We assume only one video track return this.stream.getVideoTracks().length === 0 || this.videoMuted; } - isSpeaking() { return this.speaking; } + + /** + * Replaces the current MediaStream with a new one. + * The stream will be different and new stream as remote parties are + * concerned, but this can be used for convenience locally to set up + * volume listeners automatically on the new stream etc. + * @param newStream - new stream with which to replace the current one + */ + setNewStream(newStream) { + this.updateStream(this.stream, newStream); + } + /** * Set one or both of feed's internal audio and video video mute state * Either value may be null to leave it as-is - * @param muted is the feed's video muted? + * @param audioMuted - is the feed's audio muted? + * @param videoMuted - is the feed's video muted? */ - - setAudioVideoMuted(audioMuted, videoMuted) { if (audioMuted !== null) { if (this.audioMuted !== audioMuted) { this.speakingVolumeSamples.fill(-Infinity); } - this.audioMuted = audioMuted; } - if (videoMuted !== null) this.videoMuted = videoMuted; this.emit(CallFeedEvent.MuteStateChanged, this.audioMuted, this.videoMuted); } + /** * Starts emitting volume_changed events where the emitter value is in decibels - * @param enabled emit volume changes + * @param enabled - emit volume changes */ - - measureVolumeActivity(enabled) { if (enabled) { - if (!this.audioContext || !this.analyser || !this.frequencyBinCount || !this.hasAudioTrack) return; + if (!this.analyser || !this.frequencyBinCount || !this.hasAudioTrack) return; this.measuringVolumeActivity = true; this.volumeLooper(); } else { @@ -228,15 +226,53 @@ this.emit(CallFeedEvent.VolumeChanged, -Infinity); } } - setSpeakingThreshold(threshold) { this.speakingThreshold = threshold; } - + clone() { + const mediaHandler = this.client.getMediaHandler(); + const stream = this.stream.clone(); + _logger.logger.log(`CallFeed clone() cloning stream (originalStreamId=${this.stream.id}, newStreamId${stream.id})`); + if (this.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia) { + mediaHandler.userMediaStreams.push(stream); + } else { + mediaHandler.screensharingStreams.push(stream); + } + return new CallFeed({ + client: this.client, + roomId: this.roomId, + userId: this.userId, + deviceId: this.deviceId, + stream, + purpose: this.purpose, + audioMuted: this.audioMuted, + videoMuted: this.videoMuted + }); + } dispose() { clearTimeout(this.volumeLooperTimeout); + this.stream?.removeEventListener("addtrack", this.onAddTrack); + this.call?.removeListener(_call.CallEvent.State, this.onCallState); + if (this.audioContext) { + this.audioContext = undefined; + this.analyser = undefined; + (0, _audioContext.releaseContext)(); + } + this._disposed = true; + this.emit(CallFeedEvent.Disposed); + } + get disposed() { + return this._disposed; + } + set disposed(value) { + this._disposed = value; + } + getLocalVolume() { + return this.localVolume; + } + setLocalVolume(localVolume) { + this.localVolume = localVolume; + this.emit(CallFeedEvent.LocalVolumeChanged, localVolume); } - } - exports.CallFeed = CallFeed; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/call.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/call.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/call.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/call.js 2023-04-11 06:11:52.000000000 +0000 @@ -5,31 +5,40 @@ }); exports.MatrixCall = exports.CallType = exports.CallState = exports.CallParty = exports.CallEvent = exports.CallErrorCode = exports.CallError = exports.CallDirection = void 0; exports.createNewMatrixCall = createNewMatrixCall; +exports.genCallID = genCallID; +exports.setTracksEnabled = setTracksEnabled; exports.supportsMatrixCall = supportsMatrixCall; - +var _uuid = require("uuid"); +var _sdpTransform = require("sdp-transform"); var _logger = require("../logger"); - var utils = _interopRequireWildcard(require("../utils")); - var _event = require("../@types/event"); - var _randomstring = require("../randomstring"); - var _callEventTypes = require("./callEventTypes"); - var _callFeed = require("./callFeed"); - var _typedEventEmitter = require("../models/typed-event-emitter"); - +var _deviceinfo = require("../crypto/deviceinfo"); +var _groupCall = require("./groupCall"); +var _httpApi = require("../http-api"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +var MediaType; +(function (MediaType) { + MediaType["AUDIO"] = "audio"; + MediaType["VIDEO"] = "video"; +})(MediaType || (MediaType = {})); +var CodecName; // add more as needed +// Used internally to specify modifications to codec parameters in SDP +(function (CodecName) { + CodecName["OPUS"] = "opus"; +})(CodecName || (CodecName = {})); let CallState; exports.CallState = CallState; - (function (CallState) { CallState["Fledgling"] = "fledgling"; CallState["InviteSent"] = "invite_sent"; @@ -41,34 +50,26 @@ CallState["Ringing"] = "ringing"; CallState["Ended"] = "ended"; })(CallState || (exports.CallState = CallState = {})); - let CallType; exports.CallType = CallType; - (function (CallType) { CallType["Voice"] = "voice"; CallType["Video"] = "video"; })(CallType || (exports.CallType = CallType = {})); - let CallDirection; exports.CallDirection = CallDirection; - (function (CallDirection) { CallDirection["Inbound"] = "inbound"; CallDirection["Outbound"] = "outbound"; })(CallDirection || (exports.CallDirection = CallDirection = {})); - let CallParty; exports.CallParty = CallParty; - (function (CallParty) { CallParty["Local"] = "local"; CallParty["Remote"] = "remote"; })(CallParty || (exports.CallParty = CallParty = {})); - let CallEvent; exports.CallEvent = CallEvent; - (function (CallEvent) { CallEvent["Hangup"] = "hangup"; CallEvent["State"] = "state"; @@ -81,15 +82,13 @@ CallEvent["AssertedIdentityChanged"] = "asserted_identity_changed"; CallEvent["LengthChanged"] = "length_changed"; CallEvent["DataChannel"] = "datachannel"; + CallEvent["SendVoipEvent"] = "send_voip_event"; })(CallEvent || (exports.CallEvent = CallEvent = {})); - let CallErrorCode; /** * The version field that we set in m.call.* events */ - exports.CallErrorCode = CallErrorCode; - (function (CallErrorCode) { CallErrorCode["UserHangup"] = "user_hangup"; CallErrorCode["LocalOfferFailed"] = "local_offer_failed"; @@ -97,6 +96,7 @@ CallErrorCode["UnknownDevices"] = "unknown_devices"; CallErrorCode["SendInvite"] = "send_invite"; CallErrorCode["CreateAnswer"] = "create_answer"; + CallErrorCode["CreateOffer"] = "create_offer"; CallErrorCode["SendAnswer"] = "send_answer"; CallErrorCode["SetRemoteDescription"] = "set_remote_description"; CallErrorCode["SetLocalDescription"] = "set_local_description"; @@ -106,749 +106,683 @@ CallErrorCode["Replaced"] = "replaced"; CallErrorCode["SignallingFailed"] = "signalling_timeout"; CallErrorCode["UserBusy"] = "user_busy"; - CallErrorCode["Transfered"] = "transferred"; + CallErrorCode["Transferred"] = "transferred"; + CallErrorCode["NewSession"] = "new_session"; })(CallErrorCode || (exports.CallErrorCode = CallErrorCode = {})); - const VOIP_PROTO_VERSION = "1"; + /** The fallback ICE server to use for STUN or TURN protocols. */ +const FALLBACK_ICE_SERVER = "stun:turn.matrix.org"; -const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org'; /** The length of time a call can be ringing for. */ - -const CALL_TIMEOUT_MS = 60000; +const CALL_TIMEOUT_MS = 60 * 1000; // ms +/** The time after which we increment callLength */ +const CALL_LENGTH_INTERVAL = 1000; // ms +/** The time after which we end the call, if ICE got disconnected */ +const ICE_DISCONNECTED_TIMEOUT = 30 * 1000; // ms class CallError extends Error { constructor(code, msg, err) { // Still don't think there's any way to have proper nested errors super(msg + ": " + err); - _defineProperty(this, "code", void 0); - this.code = code; } - } - exports.CallError = CallError; - function genCallID() { return Date.now().toString() + (0, _randomstring.randomString)(16); } - -/** - * Construct a new Matrix Call. - * @constructor - * @param {Object} opts Config options. - * @param {string} opts.roomId The room ID for this call. - * @param {Object} opts.webRtc The WebRTC globals from the browser. - * @param {boolean} opts.forceTURN whether relay through TURN should be forced. - * @param {Object} opts.URL The URL global. - * @param {Array} opts.turnServers Optional. A list of TURN servers. - * @param {MatrixClient} opts.client The Matrix Client instance to send events to. - */ +function getCodecParamMods(isPtt) { + const mods = [{ + mediaType: "audio", + codec: "opus", + enableDtx: true, + maxAverageBitrate: isPtt ? 12000 : undefined + }]; + return mods; +} +// generates keys for the map of transceivers +// kind is unfortunately a string rather than MediaType as this is the type of +// track.kind +function getTransceiverKey(purpose, kind) { + return purpose + ":" + kind; +} class MatrixCall extends _typedEventEmitter.TypedEventEmitter { + // whether this call should have push-to-talk semantics + // This should be set by the consumer on incoming & outgoing calls. + // A queue for candidates waiting to go out. // We try to amalgamate candidates into a single candidate message where // possible + + // our transceivers for each purpose and type of media + // The party ID of the other side: undefined if we haven't chosen a partner // yet, null if we have but they didn't send a party ID. + // The logic of when & if a call is on hold is nontrivial and explained in is*OnHold // This flag represents whether we want the other party to be on hold + // the stats for the call at the point it ended. We can't get these after we // tear the call down, so we just grab a snapshot before we stop the call. // The typescript definitions have this type as 'any' :( + // Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example + // If candidates arrive before we've picked an opponent (which, in particular, // will happen if the opponent sends candidates eagerly before the user answers // the call) we buffer them up here so we can then add the ones from the party we pick + + // Used to keep the timer for the delay before actually stopping our + // video track after muting (see setLocalVideoMuted) + + // Used to allow connection without Video and Audio. To establish a webrtc connection without media a Data channel is + // needed At the moment this property is true if we allow MatrixClient with isVoipWithNoMediaAllowed = true + + /** + * Construct a new Matrix Call. + * @param opts - Config options. + */ constructor(opts) { super(); - _defineProperty(this, "roomId", void 0); - _defineProperty(this, "callId", void 0); - - _defineProperty(this, "state", CallState.Fledgling); - + _defineProperty(this, "invitee", void 0); _defineProperty(this, "hangupParty", void 0); - _defineProperty(this, "hangupReason", void 0); - _defineProperty(this, "direction", void 0); - _defineProperty(this, "ourPartyId", void 0); - + _defineProperty(this, "peerConn", void 0); + _defineProperty(this, "toDeviceSeq", 0); + _defineProperty(this, "isPtt", false); + _defineProperty(this, "_state", CallState.Fledgling); _defineProperty(this, "client", void 0); - _defineProperty(this, "forceTURN", void 0); - _defineProperty(this, "turnServers", void 0); - _defineProperty(this, "candidateSendQueue", []); - _defineProperty(this, "candidateSendTries", 0); - - _defineProperty(this, "sentEndOfCandidates", false); - - _defineProperty(this, "peerConn", void 0); - + _defineProperty(this, "candidatesEnded", false); _defineProperty(this, "feeds", []); - - _defineProperty(this, "usermediaSenders", []); - - _defineProperty(this, "screensharingSenders", []); - + _defineProperty(this, "transceivers", new Map()); _defineProperty(this, "inviteOrAnswerSent", false); - - _defineProperty(this, "waitForLocalAVStream", void 0); - + _defineProperty(this, "waitForLocalAVStream", false); _defineProperty(this, "successor", void 0); - _defineProperty(this, "opponentMember", void 0); - _defineProperty(this, "opponentVersion", void 0); - _defineProperty(this, "opponentPartyId", void 0); - _defineProperty(this, "opponentCaps", void 0); - + _defineProperty(this, "iceDisconnectedTimeout", void 0); _defineProperty(this, "inviteTimeout", void 0); - + _defineProperty(this, "removeTrackListeners", new Map()); _defineProperty(this, "remoteOnHold", false); - _defineProperty(this, "callStatsAtEnd", void 0); - _defineProperty(this, "makingOffer", false); - - _defineProperty(this, "ignoreOffer", void 0); - + _defineProperty(this, "ignoreOffer", false); + _defineProperty(this, "responsePromiseChain", void 0); _defineProperty(this, "remoteCandidateBuffer", new Map()); - _defineProperty(this, "remoteAssertedIdentity", void 0); - _defineProperty(this, "remoteSDPStreamMetadata", void 0); - _defineProperty(this, "callLengthInterval", void 0); - - _defineProperty(this, "callLength", 0); - + _defineProperty(this, "callStartTime", void 0); + _defineProperty(this, "opponentDeviceId", void 0); + _defineProperty(this, "opponentDeviceInfo", void 0); + _defineProperty(this, "opponentSessionId", void 0); + _defineProperty(this, "groupCallId", void 0); + _defineProperty(this, "stopVideoTrackTimer", void 0); + _defineProperty(this, "isOnlyDataChannelAllowed", void 0); _defineProperty(this, "gotLocalIceCandidate", event => { if (event.candidate) { - _logger.logger.debug("Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " + event.candidate.candidate); + if (this.candidatesEnded) { + _logger.logger.warn(`Call ${this.callId} gotLocalIceCandidate() got candidate after candidates have ended - ignoring!`); + return; + } + _logger.logger.debug(`Call ${this.callId} got local ICE ${event.candidate.sdpMid} ${event.candidate.candidate}`); + if (this.callHasEnded()) return; - if (this.callHasEnded()) return; // As with the offer, note we need to make a copy of this object, not + // As with the offer, note we need to make a copy of this object, not // pass the original: that broke in Chrome ~m43. - - if (event.candidate.candidate !== '' || !this.sentEndOfCandidates) { + if (event.candidate.candidate === "") { + this.queueCandidate(null); + } else { this.queueCandidate(event.candidate); - if (event.candidate.candidate === '') this.sentEndOfCandidates = true; } } }); - _defineProperty(this, "onIceGatheringStateChange", event => { - _logger.logger.debug("ice gathering state changed to " + this.peerConn.iceGatheringState); - - if (this.peerConn.iceGatheringState === 'complete' && !this.sentEndOfCandidates) { - // If we didn't get an empty-string candidate to signal the end of candidates, - // create one ourselves now gathering has finished. - // We cast because the interface lists all the properties as required but we - // only want to send 'candidate' - // XXX: We probably want to send either sdpMid or sdpMLineIndex, as it's not strictly - // correct to have a candidate that lacks both of these. We'd have to figure out what - // previous candidates had been sent with and copy them. - const c = { - candidate: '' - }; - this.queueCandidate(c); - this.sentEndOfCandidates = true; + _logger.logger.debug(`Call ${this.callId} onIceGatheringStateChange() ice gathering state changed to ${this.peerConn.iceGatheringState}`); + if (this.peerConn?.iceGatheringState === "complete") { + this.queueCandidate(null); } }); - - _defineProperty(this, "gotLocalOffer", async description => { - _logger.logger.debug("Created offer: ", description); - - if (this.callHasEnded()) { - _logger.logger.debug("Ignoring newly created offer on call ID " + this.callId + " because the call has ended"); - - return; - } - - try { - await this.peerConn.setLocalDescription(description); - } catch (err) { - _logger.logger.debug("Error setting local description!", err); - - this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true); - return; - } - - if (this.peerConn.iceGatheringState === 'gathering') { - // Allow a short time for initial candidates to be gathered - await new Promise(resolve => { - setTimeout(resolve, 200); - }); - } - - if (this.callHasEnded()) return; - const eventType = this.state === CallState.CreateOffer ? _event.EventType.CallInvite : _event.EventType.CallNegotiate; - const content = { - lifetime: CALL_TIMEOUT_MS - }; // clunky because TypeScript can't follow the types through if we use an expression as the key - - if (this.state === CallState.CreateOffer) { - content.offer = this.peerConn.localDescription; - } else { - content.description = this.peerConn.localDescription; - } - - content.capabilities = { - 'm.call.transferee': this.client.supportsCallTransfer, - 'm.call.dtmf': false - }; - content[_callEventTypes.SDPStreamMetadataKey] = this.getLocalSDPStreamMetadata(); // Get rid of any candidates waiting to be sent: they'll be included in the local - // description we just got and will send in the offer. - - _logger.logger.info(`Discarding ${this.candidateSendQueue.length} candidates that will be sent in offer`); - - this.candidateSendQueue = []; - - try { - await this.sendVoipEvent(eventType, content); - } catch (error) { - _logger.logger.error("Failed to send invite", error); - - if (error.event) this.client.cancelPendingEvent(error.event); - let code = CallErrorCode.SignallingFailed; - let message = "Signalling failed"; - - if (this.state === CallState.CreateOffer) { - code = CallErrorCode.SendInvite; - message = "Failed to send invite"; - } - - if (error.name == 'UnknownDeviceError') { - code = CallErrorCode.UnknownDevices; - message = "Unknown devices present in the room"; - } - - this.emit(CallEvent.Error, new CallError(code, message, error)); - this.terminate(CallParty.Local, code, false); // no need to carry on & send the candidate queue, but we also - // don't want to rethrow the error - - return; - } - - this.sendCandidateQueue(); - - if (this.state === CallState.CreateOffer) { - this.inviteOrAnswerSent = true; - this.setState(CallState.InviteSent); - this.inviteTimeout = setTimeout(() => { - this.inviteTimeout = null; - - if (this.state === CallState.InviteSent) { - this.hangup(CallErrorCode.InviteTimeout, false); - } - }, CALL_TIMEOUT_MS); - } - }); - _defineProperty(this, "getLocalOfferFailed", err => { - _logger.logger.error("Failed to get local offer", err); - + _logger.logger.error(`Call ${this.callId} getLocalOfferFailed() running`, err); this.emit(CallEvent.Error, new CallError(CallErrorCode.LocalOfferFailed, "Failed to get local offer!", err)); this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false); }); - _defineProperty(this, "getUserMediaFailed", err => { if (this.successor) { this.successor.getUserMediaFailed(err); return; } - - _logger.logger.warn("Failed to get user media - ending call", err); - + _logger.logger.warn(`Call ${this.callId} getUserMediaFailed() failed to get user media - ending call`, err); this.emit(CallEvent.Error, new CallError(CallErrorCode.NoUserMedia, "Couldn't start capturing media! Is your microphone set up and " + "does this app have permission?", err)); this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false); }); - _defineProperty(this, "onIceConnectionStateChanged", () => { if (this.callHasEnded()) { return; // because ICE can still complete as we're ending the call } - _logger.logger.debug("Call ID " + this.callId + ": ICE connection state changed to: " + this.peerConn.iceConnectionState); // ideally we'd consider the call to be connected when we get media but - // chrome doesn't implement any of the 'onstarted' events yet - + _logger.logger.debug(`Call ${this.callId} onIceConnectionStateChanged() running (state=${this.peerConn?.iceConnectionState})`); - if (this.peerConn.iceConnectionState == 'connected') { - this.setState(CallState.Connected); - - if (!this.callLengthInterval) { + // ideally we'd consider the call to be connected when we get media but + // chrome doesn't implement any of the 'onstarted' events yet + if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? "")) { + clearTimeout(this.iceDisconnectedTimeout); + this.iceDisconnectedTimeout = undefined; + this.state = CallState.Connected; + if (!this.callLengthInterval && !this.callStartTime) { + this.callStartTime = Date.now(); this.callLengthInterval = setInterval(() => { - this.callLength++; - this.emit(CallEvent.LengthChanged, this.callLength); - }, 1000); + this.emit(CallEvent.LengthChanged, Math.round((Date.now() - this.callStartTime) / 1000)); + }, CALL_LENGTH_INTERVAL); } - } else if (this.peerConn.iceConnectionState == 'failed') { - this.hangup(CallErrorCode.IceFailed, false); + } else if (this.peerConn?.iceConnectionState == "failed") { + // Firefox for Android does not yet have support for restartIce() + // (the types say it's always defined though, so we have to cast + // to prevent typescript from warning). + if (this.peerConn?.restartIce) { + this.candidatesEnded = false; + this.peerConn.restartIce(); + } else { + _logger.logger.info(`Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE failed and no ICE restart method)`); + this.hangup(CallErrorCode.IceFailed, false); + } + } else if (this.peerConn?.iceConnectionState == "disconnected") { + this.iceDisconnectedTimeout = setTimeout(() => { + _logger.logger.info(`Call ${this.callId} onIceConnectionStateChanged() hanging up call (ICE disconnected for too long)`); + this.hangup(CallErrorCode.IceFailed, false); + }, ICE_DISCONNECTED_TIMEOUT); + this.state = CallState.Connecting; } - }); + // In PTT mode, override feed status to muted when we lose connection to + // the peer, since we don't want to block the line if they're not saying anything. + // Experimenting in Chrome, this happens after 5 or 6 seconds, which is probably + // fast enough. + if (this.isPtt && ["failed", "disconnected"].includes(this.peerConn.iceConnectionState)) { + for (const feed of this.getRemoteFeeds()) { + feed.setAudioVideoMuted(true, true); + } + } + }); _defineProperty(this, "onSignallingStateChanged", () => { - _logger.logger.debug("call " + this.callId + ": Signalling state changed to: " + this.peerConn.signalingState); + _logger.logger.debug(`Call ${this.callId} onSignallingStateChanged() running (state=${this.peerConn?.signalingState})`); }); - _defineProperty(this, "onTrack", ev => { if (ev.streams.length === 0) { - _logger.logger.warn(`Streamless ${ev.track.kind} found: ignoring.`); - + _logger.logger.warn(`Call ${this.callId} onTrack() called with streamless track streamless (kind=${ev.track.kind})`); return; } - const stream = ev.streams[0]; this.pushRemoteFeed(stream); - stream.addEventListener("removetrack", () => { - if (stream.getTracks().length === 0) { - _logger.logger.info(`Stream ID ${stream.id} has no tracks remaining - removing`); - - this.deleteFeedByStream(stream); - } - }); + if (!this.removeTrackListeners.has(stream)) { + const onRemoveTrack = () => { + if (stream.getTracks().length === 0) { + _logger.logger.info(`Call ${this.callId} onTrack() removing track (streamId=${stream.id})`); + this.deleteFeedByStream(stream); + stream.removeEventListener("removetrack", onRemoveTrack); + this.removeTrackListeners.delete(stream); + } + }; + stream.addEventListener("removetrack", onRemoveTrack); + this.removeTrackListeners.set(stream, onRemoveTrack); + } }); - _defineProperty(this, "onDataChannel", ev => { this.emit(CallEvent.DataChannel, ev.channel); }); - _defineProperty(this, "onNegotiationNeeded", async () => { - _logger.logger.info("Negotiation is needed!"); - + _logger.logger.info(`Call ${this.callId} onNegotiationNeeded() negotiation is needed!`); if (this.state !== CallState.CreateOffer && this.opponentVersion === 0) { - _logger.logger.info("Opponent does not support renegotiation: ignoring negotiationneeded event"); - + _logger.logger.info(`Call ${this.callId} onNegotiationNeeded() opponent does not support renegotiation: ignoring negotiationneeded event`); return; } - - this.makingOffer = true; - - try { - this.getRidOfRTXCodecs(); - const myOffer = await this.peerConn.createOffer(); - await this.gotLocalOffer(myOffer); - } catch (e) { - this.getLocalOfferFailed(e); - return; - } finally { - this.makingOffer = false; - } + this.queueGotLocalOffer(); }); - _defineProperty(this, "onHangupReceived", msg => { - _logger.logger.debug("Hangup received for call ID " + this.callId); // party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen - // a partner yet but we're treating the hangup as a reject as per VoIP v0) - + _logger.logger.debug(`Call ${this.callId} onHangupReceived() running`); + // party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen + // a partner yet but we're treating the hangup as a reject as per VoIP v0) if (this.partyIdMatches(msg) || this.state === CallState.Ringing) { // default reason is user_hangup this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true); } else { - _logger.logger.info(`Ignoring message from party ID ${msg.party_id}: our partner is ${this.opponentPartyId}`); + _logger.logger.info(`Call ${this.callId} onHangupReceived() ignoring message from party ID ${msg.party_id}: our partner is ${this.opponentPartyId}`); } }); - _defineProperty(this, "onRejectReceived", msg => { - _logger.logger.debug("Reject received for call ID " + this.callId); // No need to check party_id for reject because if we'd received either - // an answer or reject, we wouldn't be in state InviteSent + _logger.logger.debug(`Call ${this.callId} onRejectReceived() running`); + // No need to check party_id for reject because if we'd received either + // an answer or reject, we wouldn't be in state InviteSent - const shouldTerminate = // reject events also end the call if it's ringing: it's another of + const shouldTerminate = + // reject events also end the call if it's ringing: it's another of // our devices rejecting the call. - [CallState.InviteSent, CallState.Ringing].includes(this.state) || // also if we're in the init state and it's an inbound call, since + [CallState.InviteSent, CallState.Ringing].includes(this.state) || + // also if we're in the init state and it's an inbound call, since // this means we just haven't entered the ringing state yet this.state === CallState.Fledgling && this.direction === CallDirection.Inbound; - if (shouldTerminate) { this.terminate(CallParty.Remote, msg.reason || CallErrorCode.UserHangup, true); } else { - _logger.logger.debug(`Call is in state: ${this.state}: ignoring reject`); + _logger.logger.debug(`Call ${this.callId} onRejectReceived() called in wrong state (state=${this.state})`); } }); - _defineProperty(this, "onAnsweredElsewhere", msg => { - _logger.logger.debug("Call ID " + this.callId + " answered elsewhere"); - + _logger.logger.debug(`Call ${this.callId} onAnsweredElsewhere() running`); this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true); }); - this.roomId = opts.roomId; + this.invitee = opts.invitee; this.client = opts.client; - this.forceTURN = opts.forceTURN; - this.ourPartyId = this.client.deviceId; // Array of Objects with urls, username, credential keys - + if (!this.client.deviceId) throw new Error("Client must have a device ID to start calls"); + this.forceTURN = opts.forceTURN ?? false; + this.ourPartyId = this.client.deviceId; + this.opponentDeviceId = opts.opponentDeviceId; + this.opponentSessionId = opts.opponentSessionId; + this.groupCallId = opts.groupCallId; + // Array of Objects with urls, username, credential keys this.turnServers = opts.turnServers || []; - if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) { this.turnServers.push({ urls: [FALLBACK_ICE_SERVER] }); } - for (const server of this.turnServers) { utils.checkObjectHasKeys(server, ["urls"]); } - this.callId = genCallID(); + // If the Client provides calls without audio and video we need a datachannel for a webrtc connection + this.isOnlyDataChannelAllowed = this.client.isVoipWithNoMediaAllowed; } + /** * Place a voice call to this room. * @throws If you have not specified a listener for 'error' events. */ - - async placeVoiceCall() { await this.placeCall(true, false); } + /** * Place a video call to this room. * @throws If you have not specified a listener for 'error' events. */ - - async placeVideoCall() { await this.placeCall(true, true); } + /** * Create a datachannel using this call's peer connection. - * @param label A human readable label for this datachannel - * @param options An object providing configuration options for the data channel. + * @param label - A human readable label for this datachannel + * @param options - An object providing configuration options for the data channel. */ - - createDataChannel(label, options) { const dataChannel = this.peerConn.createDataChannel(label, options); this.emit(CallEvent.DataChannel, dataChannel); - - _logger.logger.debug("created data channel"); - return dataChannel; } - getOpponentMember() { return this.opponentMember; } - + getOpponentDeviceId() { + return this.opponentDeviceId; + } + getOpponentSessionId() { + return this.opponentSessionId; + } opponentCanBeTransferred() { return Boolean(this.opponentCaps && this.opponentCaps["m.call.transferee"]); } - opponentSupportsDTMF() { return Boolean(this.opponentCaps && this.opponentCaps["m.call.dtmf"]); } - getRemoteAssertedIdentity() { return this.remoteAssertedIdentity; } - + get state() { + return this._state; + } + set state(state) { + const oldState = this._state; + this._state = state; + this.emit(CallEvent.State, state, oldState); + } get type() { - return this.hasLocalUserMediaVideoTrack || this.hasRemoteUserMediaVideoTrack ? CallType.Video : CallType.Voice; + // we may want to look for a video receiver here rather than a track to match the + // sender behaviour, although in practice they should be the same thing + return this.hasUserMediaVideoSender || this.hasRemoteUserMediaVideoTrack ? CallType.Video : CallType.Voice; } - get hasLocalUserMediaVideoTrack() { - return this.localUsermediaStream?.getVideoTracks().length > 0; + return !!this.localUsermediaStream?.getVideoTracks().length; } - get hasRemoteUserMediaVideoTrack() { return this.getRemoteFeeds().some(feed => { - return feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia && feed.stream.getVideoTracks().length > 0; + return feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia && feed.stream?.getVideoTracks().length; }); } - get hasLocalUserMediaAudioTrack() { - return this.localUsermediaStream?.getAudioTracks().length > 0; + return !!this.localUsermediaStream?.getAudioTracks().length; } - get hasRemoteUserMediaAudioTrack() { return this.getRemoteFeeds().some(feed => { - return feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia && feed.stream.getAudioTracks().length > 0; + return feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia && !!feed.stream?.getAudioTracks().length; }); } - + get hasUserMediaAudioSender() { + return Boolean(this.transceivers.get(getTransceiverKey(_callEventTypes.SDPStreamMetadataPurpose.Usermedia, "audio"))?.sender); + } + get hasUserMediaVideoSender() { + return Boolean(this.transceivers.get(getTransceiverKey(_callEventTypes.SDPStreamMetadataPurpose.Usermedia, "video"))?.sender); + } get localUsermediaFeed() { return this.getLocalFeeds().find(feed => feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia); } - get localScreensharingFeed() { return this.getLocalFeeds().find(feed => feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Screenshare); } - get localUsermediaStream() { return this.localUsermediaFeed?.stream; } - get localScreensharingStream() { return this.localScreensharingFeed?.stream; } - get remoteUsermediaFeed() { return this.getRemoteFeeds().find(feed => feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia); } - get remoteScreensharingFeed() { return this.getRemoteFeeds().find(feed => feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Screenshare); } - get remoteUsermediaStream() { return this.remoteUsermediaFeed?.stream; } - get remoteScreensharingStream() { return this.remoteScreensharingFeed?.stream; } - getFeedByStreamId(streamId) { return this.getFeeds().find(feed => feed.stream.id === streamId); } + /** * Returns an array of all CallFeeds - * @returns {Array} CallFeeds + * @returns CallFeeds */ - - getFeeds() { return this.feeds; } + /** * Returns an array of all local CallFeeds - * @returns {Array} local CallFeeds + * @returns local CallFeeds */ - - getLocalFeeds() { return this.feeds.filter(feed => feed.isLocal()); } + /** * Returns an array of all remote CallFeeds - * @returns {Array} remote CallFeeds + * @returns remote CallFeeds */ - - getRemoteFeeds() { return this.feeds.filter(feed => !feed.isLocal()); } + async initOpponentCrypto() { + if (!this.opponentDeviceId) return; + if (!this.client.getUseE2eForGroupCall()) return; + // It's possible to want E2EE and yet not have the means to manage E2EE + // ourselves (for example if the client is a RoomWidgetClient) + if (!this.client.isCryptoEnabled()) { + // All we know is the device ID + this.opponentDeviceInfo = new _deviceinfo.DeviceInfo(this.opponentDeviceId); + return; + } + // if we've got to this point, we do want to init crypto, so throw if we can't + if (!this.client.crypto) throw new Error("Crypto is not initialised."); + const userId = this.invitee || this.getOpponentMember()?.userId; + if (!userId) throw new Error("Couldn't find opponent user ID to init crypto"); + const deviceInfoMap = await this.client.crypto.deviceList.downloadKeys([userId], false); + this.opponentDeviceInfo = deviceInfoMap.get(userId)?.get(this.opponentDeviceId); + if (this.opponentDeviceInfo === undefined) { + throw new _groupCall.GroupCallUnknownDeviceError(userId); + } + } + /** * Generates and returns localSDPStreamMetadata - * @returns {SDPStreamMetadata} localSDPStreamMetadata + * @returns localSDPStreamMetadata */ - - - getLocalSDPStreamMetadata() { + getLocalSDPStreamMetadata(updateStreamIds = false) { const metadata = {}; - for (const localFeed of this.getLocalFeeds()) { - metadata[localFeed.stream.id] = { + if (updateStreamIds) { + localFeed.sdpMetadataStreamId = localFeed.stream.id; + } + metadata[localFeed.sdpMetadataStreamId] = { purpose: localFeed.purpose, audio_muted: localFeed.isAudioMuted(), video_muted: localFeed.isVideoMuted() }; } - - _logger.logger.debug("Got local SDPStreamMetadata", metadata); - return metadata; } + /** * Returns true if there are no incoming feeds, * otherwise returns false - * @returns {boolean} no incoming feeds + * @returns no incoming feeds */ - - noIncomingFeeds() { return !this.feeds.some(feed => !feed.isLocal()); } - pushRemoteFeed(stream) { // Fallback to old behavior if the other side doesn't support SDPStreamMetadata if (!this.opponentSupportsSDPStreamMetadata()) { this.pushRemoteFeedWithoutMetadata(stream); return; } - const userId = this.getOpponentMember().userId; const purpose = this.remoteSDPStreamMetadata[stream.id].purpose; const audioMuted = this.remoteSDPStreamMetadata[stream.id].audio_muted; const videoMuted = this.remoteSDPStreamMetadata[stream.id].video_muted; - if (!purpose) { - _logger.logger.warn(`Ignoring stream with id ${stream.id} because we didn't get any metadata about it`); - + _logger.logger.warn(`Call ${this.callId} pushRemoteFeed() ignoring stream because we didn't get any metadata about it (streamId=${stream.id})`); return; } - if (this.getFeedByStreamId(stream.id)) { - _logger.logger.warn(`Ignoring stream with id ${stream.id} because we already have a feed for it`); - + _logger.logger.warn(`Call ${this.callId} pushRemoteFeed() ignoring stream because we already have a feed for it (streamId=${stream.id})`); return; } - this.feeds.push(new _callFeed.CallFeed({ client: this.client, + call: this, roomId: this.roomId, userId, + deviceId: this.getOpponentDeviceId(), stream, purpose, audioMuted, videoMuted })); this.emit(CallEvent.FeedsChanged, this.feeds); - - _logger.logger.info(`Pushed remote stream (id="${stream.id}", active="${stream.active}", purpose=${purpose})`); + _logger.logger.info(`Call ${this.callId} pushRemoteFeed() pushed stream (streamId=${stream.id}, active=${stream.active}, purpose=${purpose})`); } + /** * This method is used ONLY if the other client doesn't support sending SDPStreamMetadata */ - - pushRemoteFeedWithoutMetadata(stream) { - const userId = this.getOpponentMember().userId; // We can guess the purpose here since the other client can only send one stream - + const userId = this.getOpponentMember().userId; + // We can guess the purpose here since the other client can only send one stream const purpose = _callEventTypes.SDPStreamMetadataPurpose.Usermedia; - const oldRemoteStream = this.feeds.find(feed => !feed.isLocal())?.stream; // Note that we check by ID and always set the remote stream: Chrome appears + const oldRemoteStream = this.feeds.find(feed => !feed.isLocal())?.stream; + + // Note that we check by ID and always set the remote stream: Chrome appears // to make new stream objects when transceiver directionality is changed and the 'active' // status of streams change - Dave // If we already have a stream, check this stream has the same id - if (oldRemoteStream && stream.id !== oldRemoteStream.id) { - _logger.logger.warn(`Ignoring new stream ID ${stream.id}: we already have stream ID ${oldRemoteStream.id}`); - + _logger.logger.warn(`Call ${this.callId} pushRemoteFeedWithoutMetadata() ignoring new stream because we already have stream (streamId=${stream.id})`); return; } - if (this.getFeedByStreamId(stream.id)) { - _logger.logger.warn(`Ignoring stream with id ${stream.id} because we already have a feed for it`); - + _logger.logger.warn(`Call ${this.callId} pushRemoteFeedWithoutMetadata() ignoring stream because we already have a feed for it (streamId=${stream.id})`); return; } - this.feeds.push(new _callFeed.CallFeed({ client: this.client, + call: this, roomId: this.roomId, audioMuted: false, videoMuted: false, userId, + deviceId: this.getOpponentDeviceId(), stream, purpose })); this.emit(CallEvent.FeedsChanged, this.feeds); - - _logger.logger.info(`Pushed remote stream (id="${stream.id}", active="${stream.active}")`); + _logger.logger.info(`Call ${this.callId} pushRemoteFeedWithoutMetadata() pushed stream (streamId=${stream.id}, active=${stream.active})`); } - pushNewLocalFeed(stream, purpose, addToPeerConnection = true) { - const userId = this.client.getUserId(); // Tracks don't always start off enabled, eg. chrome will give a disabled + const userId = this.client.getUserId(); + + // Tracks don't always start off enabled, eg. chrome will give a disabled // audio track if you ask for user media audio and already had one that // you'd set to disabled (presumably because it clones them internally). - setTracksEnabled(stream.getAudioTracks(), true); setTracksEnabled(stream.getVideoTracks(), true); - if (this.getFeedByStreamId(stream.id)) { - _logger.logger.warn(`Ignoring stream with id ${stream.id} because we already have a feed for it`); - + _logger.logger.warn(`Call ${this.callId} pushNewLocalFeed() ignoring stream because we already have a feed for it (streamId=${stream.id})`); return; } - this.pushLocalFeed(new _callFeed.CallFeed({ client: this.client, roomId: this.roomId, audioMuted: false, videoMuted: false, userId, + deviceId: this.getOpponentDeviceId(), stream, purpose }), addToPeerConnection); } + /** * Pushes supplied feed to the call - * @param {CallFeed} callFeed to push - * @param {boolean} addToPeerConnection whether to add the tracks to the peer connection + * @param callFeed - to push + * @param addToPeerConnection - whether to add the tracks to the peer connection */ - - pushLocalFeed(callFeed, addToPeerConnection = true) { + if (this.feeds.some(feed => callFeed.stream.id === feed.stream.id)) { + _logger.logger.info(`Call ${this.callId} pushLocalFeed() ignoring duplicate local stream (streamId=${callFeed.stream.id})`); + return; + } this.feeds.push(callFeed); - if (addToPeerConnection) { - const senderArray = callFeed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia ? this.usermediaSenders : this.screensharingSenders; // Empty the array - - senderArray.splice(0, senderArray.length); - for (const track of callFeed.stream.getTracks()) { - _logger.logger.info(`Adding track (` + `id="${track.id}", ` + `kind="${track.kind}", ` + `streamId="${callFeed.stream.id}", ` + `streamPurpose="${callFeed.purpose}", ` + `enabled=${track.enabled}` + `) to peer connection`); - - senderArray.push(this.peerConn.addTrack(track, callFeed.stream)); + _logger.logger.info(`Call ${this.callId} pushLocalFeed() adding track to peer connection (id=${track.id}, kind=${track.kind}, streamId=${callFeed.stream.id}, streamPurpose=${callFeed.purpose}, enabled=${track.enabled})`); + const tKey = getTransceiverKey(callFeed.purpose, track.kind); + if (this.transceivers.has(tKey)) { + // we already have a sender, so we re-use it. We try to re-use transceivers as much + // as possible because they can't be removed once added, so otherwise they just + // accumulate which makes the SDP very large very quickly: in fact it only takes + // about 6 video tracks to exceed the maximum size of an Olm-encrypted + // Matrix event. + const transceiver = this.transceivers.get(tKey); + + // this is what would allow us to use addTransceiver(), but it's not available + // on Firefox yet. We call it anyway if we have it. + if (transceiver.sender.setStreams) transceiver.sender.setStreams(callFeed.stream); + transceiver.sender.replaceTrack(track); + // set the direction to indicate we're going to start sending again + // (this will trigger the re-negotiation) + transceiver.direction = transceiver.direction === "inactive" ? "sendonly" : "sendrecv"; + } else { + // create a new one. We need to use addTrack rather addTransceiver for this because firefox + // doesn't yet implement RTCRTPSender.setStreams() + // (https://bugzilla.mozilla.org/show_bug.cgi?id=1510802) so we'd have no way to group the + // two tracks together into a stream. + const newSender = this.peerConn.addTrack(track, callFeed.stream); + + // now go & fish for the new transceiver + const newTransceiver = this.peerConn.getTransceivers().find(t => t.sender === newSender); + if (newTransceiver) { + this.transceivers.set(tKey, newTransceiver); + } else { + _logger.logger.warn(`Call ${this.callId} pushLocalFeed() didn't find a matching transceiver after adding track!`); + } + } } } - - _logger.logger.info(`Pushed local stream ` + `(id="${callFeed.stream.id}", ` + `active="${callFeed.stream.active}", ` + `purpose="${callFeed.purpose}")`); - + _logger.logger.info(`Call ${this.callId} pushLocalFeed() pushed stream (id=${callFeed.stream.id}, active=${callFeed.stream.active}, purpose=${callFeed.purpose})`); this.emit(CallEvent.FeedsChanged, this.feeds); } + /** * Removes local call feed from the call and its tracks from the peer * connection - * @param callFeed to remove + * @param callFeed - to remove */ - - removeLocalFeed(callFeed) { - const senderArray = callFeed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia ? this.usermediaSenders : this.screensharingSenders; - - for (const sender of senderArray) { - this.peerConn.removeTrack(sender); - } // Empty the array - - - senderArray.splice(0, senderArray.length); - this.deleteFeedByStream(callFeed.stream); + const audioTransceiverKey = getTransceiverKey(callFeed.purpose, "audio"); + const videoTransceiverKey = getTransceiverKey(callFeed.purpose, "video"); + for (const transceiverKey of [audioTransceiverKey, videoTransceiverKey]) { + // this is slightly mixing the track and transceiver API but is basically just shorthand. + // There is no way to actually remove a transceiver, so this just sets it to inactive + // (or recvonly) and replaces the source with nothing. + if (this.transceivers.has(transceiverKey)) { + const transceiver = this.transceivers.get(transceiverKey); + if (transceiver.sender) this.peerConn.removeTrack(transceiver.sender); + } + } + if (callFeed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Screenshare) { + this.client.getMediaHandler().stopScreensharingStream(callFeed.stream); + } + this.deleteFeed(callFeed); } - deleteAllFeeds() { for (const feed of this.feeds) { - feed.dispose(); + if (!feed.isLocal() || !this.groupCallId) { + feed.dispose(); + } } - this.feeds = []; this.emit(CallEvent.FeedsChanged, this.feeds); } - deleteFeedByStream(stream) { - _logger.logger.debug(`Removing feed with stream id ${stream.id}`); - const feed = this.getFeedByStreamId(stream.id); - if (!feed) { - _logger.logger.warn(`Didn't find the feed with stream id ${stream.id} to delete`); - + _logger.logger.warn(`Call ${this.callId} deleteFeedByStream() didn't find the feed to delete (streamId=${stream.id})`); return; } - + this.deleteFeed(feed); + } + deleteFeed(feed) { feed.dispose(); this.feeds.splice(this.feeds.indexOf(feed), 1); this.emit(CallEvent.FeedsChanged, this.feeds); - } // The typescript definitions have this type as 'any' :( - + } + // The typescript definitions have this type as 'any' :( async getCurrentCallStats() { if (this.callHasEnded()) { return this.callStatsAtEnd; } - return this.collectCallStats(); } - async collectCallStats() { // This happens when the call fails before it starts. // For example when we fail to get capture sources @@ -860,125 +794,115 @@ }); return stats; } + /** * Configure this call from an invite event. Used by MatrixClient. - * @param {MatrixEvent} event The m.call.invite event + * @param event - The m.call.invite event */ - - async initWithInvite(event) { const invite = event.getContent(); - this.direction = CallDirection.Inbound; // make sure we have valid turn creds. Unless something's gone wrong, it should - // poll and keep the credentials valid so this should be instant. + this.direction = CallDirection.Inbound; + // make sure we have valid turn creds. Unless something's gone wrong, it should + // poll and keep the credentials valid so this should be instant. const haveTurnCreds = await this.client.checkTurnServers(); - if (!haveTurnCreds) { - _logger.logger.warn("Failed to get TURN credentials! Proceeding with call anyway..."); + _logger.logger.warn(`Call ${this.callId} initWithInvite() failed to get TURN credentials! Proceeding with call anyway...`); } - const sdpStreamMetadata = invite[_callEventTypes.SDPStreamMetadataKey]; - if (sdpStreamMetadata) { this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); } else { - _logger.logger.debug("Did not get any SDPStreamMetadata! Can not send/receive multiple streams"); + _logger.logger.debug(`Call ${this.callId} initWithInvite() did not get any SDPStreamMetadata! Can not send/receive multiple streams`); } - - this.peerConn = this.createPeerConnection(); // we must set the party ID before await-ing on anything: the call event + this.peerConn = this.createPeerConnection(); + // we must set the party ID before await-ing on anything: the call event // handler will start giving us more call events (eg. candidates) so if // we haven't set the party ID, we'll ignore them. - this.chooseOpponent(event); - + await this.initOpponentCrypto(); try { await this.peerConn.setRemoteDescription(invite.offer); await this.addBufferedIceCandidates(); } catch (e) { - _logger.logger.debug("Failed to set remote description", e); - + _logger.logger.debug(`Call ${this.callId} initWithInvite() failed to set remote description`, e); this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false); return; } + const remoteStream = this.feeds.find(feed => !feed.isLocal())?.stream; - const remoteStream = this.feeds.find(feed => !feed.isLocal())?.stream; // According to previous comments in this file, firefox at some point did not + // According to previous comments in this file, firefox at some point did not // add streams until media started arriving on them. Testing latest firefox // (81 at time of writing), this is no longer a problem, so let's do it the correct way. - - if (!remoteStream || remoteStream.getTracks().length === 0) { - _logger.logger.error("No remote stream or no tracks after setting remote description!"); - + // + // For example in case of no media webrtc connections like screen share only call we have to allow webrtc + // connections without remote media. In this case we always use a data channel. At the moment we allow as well + // only data channel as media in the WebRTC connection with this setup here. + if (!this.isOnlyDataChannelAllowed && (!remoteStream || remoteStream.getTracks().length === 0)) { + _logger.logger.error(`Call ${this.callId} initWithInvite() no remote stream or no tracks after setting remote description!`); this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false); return; } - - this.setState(CallState.Ringing); - + this.state = CallState.Ringing; if (event.getLocalAge()) { - setTimeout(() => { + // Time out the call if it's ringing for too long + const ringingTimer = setTimeout(() => { if (this.state == CallState.Ringing) { - _logger.logger.debug("Call invite has expired. Hanging up."); - + _logger.logger.debug(`Call ${this.callId} initWithInvite() invite has expired. Hanging up.`); this.hangupParty = CallParty.Remote; // effectively - - this.setState(CallState.Ended); + this.state = CallState.Ended; this.stopAllMedia(); - - if (this.peerConn.signalingState != 'closed') { + if (this.peerConn.signalingState != "closed") { this.peerConn.close(); } - - this.emit(CallEvent.Hangup); + this.emit(CallEvent.Hangup, this); } }, invite.lifetime - event.getLocalAge()); + const onState = state => { + if (state !== CallState.Ringing) { + clearTimeout(ringingTimer); + this.off(CallEvent.State, onState); + } + }; + this.on(CallEvent.State, onState); } } + /** * Configure this call from a hangup or reject event. Used by MatrixClient. - * @param {MatrixEvent} event The m.call.hangup event + * @param event - The m.call.hangup event */ - - initWithHangup(event) { // perverse as it may seem, sometimes we want to instantiate a call with a // hangup message (because when getting the state of the room on load, events // come in reverse order and we want to remember that a call has been hung up) - this.setState(CallState.Ended); + this.state = CallState.Ended; } - shouldAnswerWithMediaType(wantedValue, valueOfTheOtherSide, type) { if (wantedValue && !valueOfTheOtherSide) { // TODO: Figure out how to do this - _logger.logger.warn(`Unable to answer with ${type} because the other side isn't sending it either.`); - + _logger.logger.warn(`Call ${this.callId} shouldAnswerWithMediaType() unable to answer with ${type} because the other side isn't sending it either.`); return false; } else if (!utils.isNullOrUndefined(wantedValue) && wantedValue !== valueOfTheOtherSide && !this.opponentSupportsSDPStreamMetadata()) { - _logger.logger.warn(`Unable to answer with ${type}=${wantedValue} because the other side doesn't support it. ` + `Answering with ${type}=${valueOfTheOtherSide}.`); - + _logger.logger.warn(`Call ${this.callId} shouldAnswerWithMediaType() unable to answer with ${type}=${wantedValue} because the other side doesn't support it. Answering with ${type}=${valueOfTheOtherSide}.`); return valueOfTheOtherSide; } - return wantedValue ?? valueOfTheOtherSide; } + /** * Answer a call. */ - - async answer(audio, video) { - if (this.inviteOrAnswerSent) return; // TODO: Figure out how to do this - + if (this.inviteOrAnswerSent) return; + // TODO: Figure out how to do this if (audio === false && video === false) throw new Error("You CANNOT answer a call without media"); - - _logger.logger.debug(`Answering call ${this.callId}`); - if (!this.localUsermediaStream && !this.waitForLocalAVStream) { const prevState = this.state; const answerWithAudio = this.shouldAnswerWithMediaType(audio, this.hasRemoteUserMediaAudioTrack, "audio"); const answerWithVideo = this.shouldAnswerWithMediaType(video, this.hasRemoteUserMediaVideoTrack, "video"); - this.setState(CallState.WaitLocalMedia); + this.state = CallState.WaitLocalMedia; this.waitForLocalAVStream = true; - try { const stream = await this.client.getMediaHandler().getUserMediaStream(answerWithAudio, answerWithVideo); this.waitForLocalAVStream = false; @@ -986,24 +910,22 @@ client: this.client, roomId: this.roomId, userId: this.client.getUserId(), + deviceId: this.client.getDeviceId() ?? undefined, stream, purpose: _callEventTypes.SDPStreamMetadataPurpose.Usermedia, audioMuted: false, videoMuted: false }); const feeds = [usermediaFeed]; - if (this.localScreensharingFeed) { feeds.push(this.localScreensharingFeed); } - this.answerWithCallFeeds(feeds); } catch (e) { if (answerWithVideo) { // Try to answer without video - _logger.logger.warn("Failed to getUserMedia(), trying to getUserMedia() without video"); - - this.setState(prevState); + _logger.logger.warn(`Call ${this.callId} answer() failed to getUserMedia(), trying to getUserMedia() without video`); + this.state = prevState; this.waitForLocalAVStream = false; await this.answer(answerWithAudio, false); } else { @@ -1012,420 +934,428 @@ } } } else if (this.waitForLocalAVStream) { - this.setState(CallState.WaitLocalMedia); + this.state = CallState.WaitLocalMedia; } } - answerWithCallFeeds(callFeeds) { if (this.inviteOrAnswerSent) return; - this.gotCallFeedsForAnswer(callFeeds); + this.queueGotCallFeedsForAnswer(callFeeds); } + /** * Replace this call with a new call, e.g. for glare resolution. Used by * MatrixClient. - * @param {MatrixCall} newCall The new call. + * @param newCall - The new call. */ - - replacedBy(newCall) { + _logger.logger.debug(`Call ${this.callId} replacedBy() running (newCallId=${newCall.callId})`); if (this.state === CallState.WaitLocalMedia) { - _logger.logger.debug("Telling new call to wait for local media"); - + _logger.logger.debug(`Call ${this.callId} replacedBy() telling new call to wait for local media (newCallId=${newCall.callId})`); newCall.waitForLocalAVStream = true; } else if ([CallState.CreateOffer, CallState.InviteSent].includes(this.state)) { - _logger.logger.debug("Handing local stream to new call"); - - newCall.gotCallFeedsForAnswer(this.getLocalFeeds()); + if (newCall.direction === CallDirection.Outbound) { + newCall.queueGotCallFeedsForAnswer([]); + } else { + _logger.logger.debug(`Call ${this.callId} replacedBy() handing local stream to new call(newCallId=${newCall.callId})`); + newCall.queueGotCallFeedsForAnswer(this.getLocalFeeds().map(feed => feed.clone())); + } } - this.successor = newCall; this.emit(CallEvent.Replaced, newCall); this.hangup(CallErrorCode.Replaced, true); } + /** * Hangup a call. - * @param {string} reason The reason why the call is being hung up. - * @param {boolean} suppressEvent True to suppress emitting an event. + * @param reason - The reason why the call is being hung up. + * @param suppressEvent - True to suppress emitting an event. */ - - hangup(reason, suppressEvent) { if (this.callHasEnded()) return; - - _logger.logger.debug("Ending call " + this.callId); - - this.terminate(CallParty.Local, reason, !suppressEvent); // We don't want to send hangup here if we didn't even get to sending an invite - - if (this.state === CallState.WaitLocalMedia) return; - const content = {}; // Don't send UserHangup reason to older clients - + _logger.logger.debug(`Call ${this.callId} hangup() ending call (reason=${reason})`); + this.terminate(CallParty.Local, reason, !suppressEvent); + // We don't want to send hangup here if we didn't even get to sending an invite + if ([CallState.Fledgling, CallState.WaitLocalMedia].includes(this.state)) return; + const content = {}; + // Don't send UserHangup reason to older clients if (this.opponentVersion && this.opponentVersion !== 0 || reason !== CallErrorCode.UserHangup) { content["reason"] = reason; } - this.sendVoipEvent(_event.EventType.CallHangup, content); } + /** * Reject a call * This used to be done by calling hangup, but is a separate method and protocol * event as of MSC2746. */ - - reject() { if (this.state !== CallState.Ringing) { throw Error("Call must be in 'ringing' state to reject!"); } - if (this.opponentVersion === 0) { - _logger.logger.info(`Opponent version is less than 1 (${this.opponentVersion}): sending hangup instead of reject`); - + _logger.logger.info(`Call ${this.callId} reject() opponent version is less than 1: sending hangup instead of reject (opponentVersion=${this.opponentVersion})`); this.hangup(CallErrorCode.UserHangup, true); return; } - _logger.logger.debug("Rejecting call: " + this.callId); - this.terminate(CallParty.Local, CallErrorCode.UserHangup, true); this.sendVoipEvent(_event.EventType.CallReject, {}); } + /** * Adds an audio and/or video track - upgrades the call - * @param {boolean} audio should add an audio track - * @param {boolean} video should add an video track + * @param audio - should add an audio track + * @param video - should add an video track */ - - async upgradeCall(audio, video) { // We don't do call downgrades if (!audio && !video) return; if (!this.opponentSupportsSDPStreamMetadata()) return; - try { + _logger.logger.debug(`Call ${this.callId} upgradeCall() upgrading call (audio=${audio}, video=${video})`); const getAudio = audio || this.hasLocalUserMediaAudioTrack; - const getVideo = video || this.hasLocalUserMediaVideoTrack; // updateLocalUsermediaStream() will take the tracks, use them as - // replacement and throw the stream away, so it isn't reusable + const getVideo = video || this.hasLocalUserMediaVideoTrack; + // updateLocalUsermediaStream() will take the tracks, use them as + // replacement and throw the stream away, so it isn't reusable const stream = await this.client.getMediaHandler().getUserMediaStream(getAudio, getVideo, false); await this.updateLocalUsermediaStream(stream, audio, video); } catch (error) { - _logger.logger.error("Failed to upgrade the call", error); - + _logger.logger.error(`Call ${this.callId} upgradeCall() failed to upgrade the call`, error); this.emit(CallEvent.Error, new CallError(CallErrorCode.NoUserMedia, "Failed to get camera access: ", error)); } } + /** * Returns true if this.remoteSDPStreamMetadata is defined, otherwise returns false - * @returns {boolean} can screenshare + * @returns can screenshare */ - - opponentSupportsSDPStreamMetadata() { return Boolean(this.remoteSDPStreamMetadata); } + /** * If there is a screensharing stream returns true, otherwise returns false - * @returns {boolean} is screensharing + * @returns is screensharing */ - - isScreensharing() { return Boolean(this.localScreensharingStream); } + /** * Starts/stops screensharing - * @param enabled the desired screensharing state - * @param {string} desktopCapturerSourceId optional id of the desktop capturer source to use - * @returns {boolean} new screensharing state + * @param enabled - the desired screensharing state + * @param desktopCapturerSourceId - optional id of the desktop capturer source to use + * @returns new screensharing state */ - - - async setScreensharingEnabled(enabled, desktopCapturerSourceId) { + async setScreensharingEnabled(enabled, opts) { // Skip if there is nothing to do if (enabled && this.isScreensharing()) { - _logger.logger.warn(`There is already a screensharing stream - there is nothing to do!`); - + _logger.logger.warn(`Call ${this.callId} setScreensharingEnabled() there is already a screensharing stream - there is nothing to do!`); return true; } else if (!enabled && !this.isScreensharing()) { - _logger.logger.warn(`There already isn't a screensharing stream - there is nothing to do!`); - + _logger.logger.warn(`Call ${this.callId} setScreensharingEnabled() there already isn't a screensharing stream - there is nothing to do!`); return false; - } // Fallback to replaceTrack() - + } + // Fallback to replaceTrack() if (!this.opponentSupportsSDPStreamMetadata()) { - return this.setScreensharingEnabledWithoutMetadataSupport(enabled, desktopCapturerSourceId); + return this.setScreensharingEnabledWithoutMetadataSupport(enabled, opts); } - - _logger.logger.debug(`Set screensharing enabled? ${enabled}`); - + _logger.logger.debug(`Call ${this.callId} setScreensharingEnabled() running (enabled=${enabled})`); if (enabled) { try { - const stream = await this.client.getMediaHandler().getScreensharingStream(desktopCapturerSourceId); + const stream = await this.client.getMediaHandler().getScreensharingStream(opts); if (!stream) return false; this.pushNewLocalFeed(stream, _callEventTypes.SDPStreamMetadataPurpose.Screenshare); return true; } catch (err) { - _logger.logger.error("Failed to get screen-sharing stream:", err); - + _logger.logger.error(`Call ${this.callId} setScreensharingEnabled() failed to get screen-sharing stream:`, err); return false; } } else { - for (const sender of this.screensharingSenders) { - this.peerConn.removeTrack(sender); + const audioTransceiver = this.transceivers.get(getTransceiverKey(_callEventTypes.SDPStreamMetadataPurpose.Screenshare, "audio")); + const videoTransceiver = this.transceivers.get(getTransceiverKey(_callEventTypes.SDPStreamMetadataPurpose.Screenshare, "video")); + for (const transceiver of [audioTransceiver, videoTransceiver]) { + // this is slightly mixing the track and transceiver API but is basically just shorthand + // for removing the sender. + if (transceiver && transceiver.sender) this.peerConn.removeTrack(transceiver.sender); } - this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream); this.deleteFeedByStream(this.localScreensharingStream); return false; } } + /** * Starts/stops screensharing * Should be used ONLY if the opponent doesn't support SDPStreamMetadata - * @param enabled the desired screensharing state - * @param {string} desktopCapturerSourceId optional id of the desktop capturer source to use - * @returns {boolean} new screensharing state + * @param enabled - the desired screensharing state + * @param desktopCapturerSourceId - optional id of the desktop capturer source to use + * @returns new screensharing state */ - - - async setScreensharingEnabledWithoutMetadataSupport(enabled, desktopCapturerSourceId) { - _logger.logger.debug(`Set screensharing enabled? ${enabled} using replaceTrack()`); - + async setScreensharingEnabledWithoutMetadataSupport(enabled, opts) { + _logger.logger.debug(`Call ${this.callId} setScreensharingEnabledWithoutMetadataSupport() running (enabled=${enabled})`); if (enabled) { try { - const stream = await this.client.getMediaHandler().getScreensharingStream(desktopCapturerSourceId); + const stream = await this.client.getMediaHandler().getScreensharingStream(opts); if (!stream) return false; - const track = stream.getTracks().find(track => { - return track.kind === "video"; - }); - const sender = this.usermediaSenders.find(sender => { - return sender.track?.kind === "video"; - }); - sender.replaceTrack(track); + const track = stream.getTracks().find(track => track.kind === "video"); + const sender = this.transceivers.get(getTransceiverKey(_callEventTypes.SDPStreamMetadataPurpose.Usermedia, "video"))?.sender; + sender?.replaceTrack(track ?? null); this.pushNewLocalFeed(stream, _callEventTypes.SDPStreamMetadataPurpose.Screenshare, false); return true; } catch (err) { - _logger.logger.error("Failed to get screen-sharing stream:", err); - + _logger.logger.error(`Call ${this.callId} setScreensharingEnabledWithoutMetadataSupport() failed to get screen-sharing stream:`, err); return false; } } else { - const track = this.localUsermediaStream.getTracks().find(track => { - return track.kind === "video"; - }); - const sender = this.usermediaSenders.find(sender => { - return sender.track?.kind === "video"; - }); - sender.replaceTrack(track); + const track = this.localUsermediaStream?.getTracks().find(track => track.kind === "video"); + const sender = this.transceivers.get(getTransceiverKey(_callEventTypes.SDPStreamMetadataPurpose.Usermedia, "video"))?.sender; + sender?.replaceTrack(track ?? null); this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream); this.deleteFeedByStream(this.localScreensharingStream); return false; } } + /** * Replaces/adds the tracks from the passed stream to the localUsermediaStream - * @param {MediaStream} stream to use a replacement for the local usermedia stream + * @param stream - to use a replacement for the local usermedia stream */ - - async updateLocalUsermediaStream(stream, forceAudio = false, forceVideo = false) { const callFeed = this.localUsermediaFeed; const audioEnabled = forceAudio || !callFeed.isAudioMuted() && !this.remoteOnHold; const videoEnabled = forceVideo || !callFeed.isVideoMuted() && !this.remoteOnHold; + _logger.logger.log(`Call ${this.callId} updateLocalUsermediaStream() running (streamId=${stream.id}, audio=${audioEnabled}, video=${videoEnabled})`); setTracksEnabled(stream.getAudioTracks(), audioEnabled); - setTracksEnabled(stream.getVideoTracks(), videoEnabled); // We want to keep the same stream id, so we replace the tracks rather than the whole stream + setTracksEnabled(stream.getVideoTracks(), videoEnabled); + // We want to keep the same stream id, so we replace the tracks rather + // than the whole stream. + + // Firstly, we replace the tracks in our localUsermediaStream. for (const track of this.localUsermediaStream.getTracks()) { this.localUsermediaStream.removeTrack(track); track.stop(); } - for (const track of stream.getTracks()) { this.localUsermediaStream.addTrack(track); } - const newSenders = []; - + // Then replace the old tracks, if possible. for (const track of stream.getTracks()) { - const oldSender = this.usermediaSenders.find(sender => sender.track?.kind === track.kind); - let newSender; - + const tKey = getTransceiverKey(_callEventTypes.SDPStreamMetadataPurpose.Usermedia, track.kind); + const transceiver = this.transceivers.get(tKey); + const oldSender = transceiver?.sender; + let added = false; if (oldSender) { - _logger.logger.info(`Replacing track (` + `id="${track.id}", ` + `kind="${track.kind}", ` + `streamId="${stream.id}", ` + `streamPurpose="${callFeed.purpose}"` + `) to peer connection`); - - await oldSender.replaceTrack(track); - newSender = oldSender; - } else { - _logger.logger.info(`Adding track (` + `id="${track.id}", ` + `kind="${track.kind}", ` + `streamId="${stream.id}", ` + `streamPurpose="${callFeed.purpose}"` + `) to peer connection`); - - newSender = this.peerConn.addTrack(track, this.localUsermediaStream); + try { + _logger.logger.info(`Call ${this.callId} updateLocalUsermediaStream() replacing track (id=${track.id}, kind=${track.kind}, streamId=${stream.id}, streamPurpose=${callFeed.purpose})`); + await oldSender.replaceTrack(track); + // Set the direction to indicate we're going to be sending. + // This is only necessary in the cases where we're upgrading + // the call to video after downgrading it. + transceiver.direction = transceiver.direction === "inactive" ? "sendonly" : "sendrecv"; + added = true; + } catch (error) { + _logger.logger.warn(`Call ${this.callId} updateLocalUsermediaStream() replaceTrack failed: adding new transceiver instead`, error); + } + } + if (!added) { + _logger.logger.info(`Call ${this.callId} updateLocalUsermediaStream() adding track to peer connection (id=${track.id}, kind=${track.kind}, streamId=${stream.id}, streamPurpose=${callFeed.purpose})`); + const newSender = this.peerConn.addTrack(track, this.localUsermediaStream); + const newTransceiver = this.peerConn.getTransceivers().find(t => t.sender === newSender); + if (newTransceiver) { + this.transceivers.set(tKey, newTransceiver); + } else { + _logger.logger.warn(`Call ${this.callId} updateLocalUsermediaStream() couldn't find matching transceiver for newly added track!`); + } } - - newSenders.push(newSender); } - - this.usermediaSenders = newSenders; } + /** * Set whether our outbound video should be muted or not. - * @param {boolean} muted True to mute the outbound video. + * @param muted - True to mute the outbound video. * @returns the new mute state */ - - async setLocalVideoMuted(muted) { + _logger.logger.log(`Call ${this.callId} setLocalVideoMuted() running ${muted}`); + + // if we were still thinking about stopping and removing the video + // track: don't, because we want it back. + if (!muted && this.stopVideoTrackTimer !== undefined) { + clearTimeout(this.stopVideoTrackTimer); + this.stopVideoTrackTimer = undefined; + } if (!(await this.client.getMediaHandler().hasVideoDevice())) { return this.isLocalVideoMuted(); } - - if (!this.hasLocalUserMediaVideoTrack && !muted) { + if (!this.hasUserMediaVideoSender && !muted) { await this.upgradeCall(false, true); return this.isLocalVideoMuted(); } + // we may not have a video track - if not, re-request usermedia + if (!muted && this.localUsermediaStream.getVideoTracks().length === 0) { + const stream = await this.client.getMediaHandler().getUserMediaStream(true, true); + await this.updateLocalUsermediaStream(stream); + } this.localUsermediaFeed?.setAudioVideoMuted(null, muted); this.updateMuteStatus(); + await this.sendMetadataUpdate(); + + // if we're muting video, set a timeout to stop & remove the video track so we release + // the camera. We wait a short time to do this because when we disable a track, WebRTC + // will send black video for it. If we just stop and remove it straight away, the video + // will just freeze which means that when we unmute video, the other side will briefly + // get a static frame of us from before we muted. This way, the still frame is just black. + // A very small delay is not always enough so the theory here is that it needs to be long + // enough for WebRTC to encode a frame: 120ms should be long enough even if we're only + // doing 10fps. + if (muted) { + this.stopVideoTrackTimer = setTimeout(() => { + for (const t of this.localUsermediaStream.getVideoTracks()) { + t.stop(); + this.localUsermediaStream.removeTrack(t); + } + }, 120); + } return this.isLocalVideoMuted(); } + /** * Check if local video is muted. * * If there are multiple video tracks, all of the tracks need to be muted * for this to return true. This means if there are no video tracks, this will * return true. - * @return {Boolean} True if the local preview video is muted, else false + * @returns True if the local preview video is muted, else false * (including if the call is not set up yet). */ - - isLocalVideoMuted() { - return this.localUsermediaFeed?.isVideoMuted(); + return this.localUsermediaFeed?.isVideoMuted() ?? false; } + /** * Set whether the microphone should be muted or not. - * @param {boolean} muted True to mute the mic. + * @param muted - True to mute the mic. * @returns the new mute state */ - - async setMicrophoneMuted(muted) { + _logger.logger.log(`Call ${this.callId} setMicrophoneMuted() running ${muted}`); if (!(await this.client.getMediaHandler().hasAudioDevice())) { return this.isMicrophoneMuted(); } - - if (!this.hasLocalUserMediaAudioTrack && !muted) { + if (!muted && (!this.hasUserMediaAudioSender || !this.hasLocalUserMediaAudioTrack)) { await this.upgradeCall(true, false); return this.isMicrophoneMuted(); } - this.localUsermediaFeed?.setAudioVideoMuted(muted, null); this.updateMuteStatus(); + await this.sendMetadataUpdate(); return this.isMicrophoneMuted(); } + /** * Check if the microphone is muted. * * If there are multiple audio tracks, all of the tracks need to be muted * for this to return true. This means if there are no audio tracks, this will * return true. - * @return {Boolean} True if the mic is muted, else false (including if the call + * @returns True if the mic is muted, else false (including if the call * is not set up yet). */ - - isMicrophoneMuted() { - return this.localUsermediaFeed?.isAudioMuted(); + return this.localUsermediaFeed?.isAudioMuted() ?? false; } + /** * @returns true if we have put the party on the other side of the call on hold * (that is, we are signalling to them that we are not listening) */ - - isRemoteOnHold() { return this.remoteOnHold; } - setRemoteOnHold(onHold) { if (this.isRemoteOnHold() === onHold) return; this.remoteOnHold = onHold; - for (const transceiver of this.peerConn.getTransceivers()) { // We don't send hold music or anything so we're not actually // sending anything, but sendrecv is fairly standard for hold and // it makes it a lot easier to figure out who's put who on hold. - transceiver.direction = onHold ? 'sendonly' : 'sendrecv'; + transceiver.direction = onHold ? "sendonly" : "sendrecv"; } - this.updateMuteStatus(); + this.sendMetadataUpdate(); this.emit(CallEvent.RemoteHoldUnhold, this.remoteOnHold); } + /** * Indicates whether we are 'on hold' to the remote party (ie. if true, * they cannot hear us). * @returns true if the other party has put us on hold */ - - isLocalOnHold() { if (this.state !== CallState.Connected) return false; - let callOnHold = true; // We consider a call to be on hold only if *all* the tracks are on hold - // (is this the right thing to do?) + let callOnHold = true; + // We consider a call to be on hold only if *all* the tracks are on hold + // (is this the right thing to do?) for (const transceiver of this.peerConn.getTransceivers()) { - const trackOnHold = ['inactive', 'recvonly'].includes(transceiver.currentDirection); + const trackOnHold = ["inactive", "recvonly"].includes(transceiver.currentDirection); if (!trackOnHold) callOnHold = false; } - return callOnHold; } + /** * Sends a DTMF digit to the other party - * @param digit The digit (nb. string - '#' and '*' are dtmf too) + * @param digit - The digit (nb. string - '#' and '*' are dtmf too) */ - - sendDtmfDigit(digit) { for (const sender of this.peerConn.getSenders()) { - if (sender.track.kind === 'audio' && sender.dtmf) { + if (sender.track?.kind === "audio" && sender.dtmf) { sender.dtmf.insertDTMF(digit); return; } } - throw new Error("Unable to find a track to send DTMF on"); } - updateMuteStatus() { - this.sendVoipEvent(_event.EventType.CallSDPStreamMetadataChangedPrefix, { - [_callEventTypes.SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata() - }); const micShouldBeMuted = this.isMicrophoneMuted() || this.remoteOnHold; const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold; + _logger.logger.log(`Call ${this.callId} updateMuteStatus stream ${this.localUsermediaStream.id} micShouldBeMuted ${micShouldBeMuted} vidShouldBeMuted ${vidShouldBeMuted}`); setTracksEnabled(this.localUsermediaStream.getAudioTracks(), !micShouldBeMuted); setTracksEnabled(this.localUsermediaStream.getVideoTracks(), !vidShouldBeMuted); } - - gotCallFeedsForInvite(callFeeds) { + async sendMetadataUpdate() { + await this.sendVoipEvent(_event.EventType.CallSDPStreamMetadataChangedPrefix, { + [_callEventTypes.SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata() + }); + } + gotCallFeedsForInvite(callFeeds, requestScreenshareFeed = false) { if (this.successor) { - this.successor.gotCallFeedsForAnswer(callFeeds); + this.successor.queueGotCallFeedsForAnswer(callFeeds); return; } - if (this.callHasEnded()) { this.stopAllMedia(); return; } - for (const feed of callFeeds) { this.pushLocalFeed(feed); } - - this.setState(CallState.CreateOffer); - - _logger.logger.debug("gotUserMediaForInvite"); // Now we wait for the negotiationneeded event - + if (requestScreenshareFeed) { + this.peerConn.addTransceiver("video", { + direction: "recvonly" + }); + } + this.state = CallState.CreateOffer; + _logger.logger.debug(`Call ${this.callId} gotUserMediaForInvite() run`); + // Now we wait for the negotiationneeded event } async sendAnswer() { @@ -1436,168 +1366,207 @@ // required to still be sent for backwards compat type: this.peerConn.localDescription.type }, - [_callEventTypes.SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata() + [_callEventTypes.SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(true) }; answerContent.capabilities = { - 'm.call.transferee': this.client.supportsCallTransfer, - 'm.call.dtmf': false - }; // We have just taken the local description from the peerConn which will + "m.call.transferee": this.client.supportsCallTransfer, + "m.call.dtmf": false + }; + + // We have just taken the local description from the peerConn which will // contain all the local candidates added so far, so we can discard any candidates // we had queued up because they'll be in the answer. - - _logger.logger.info(`Discarding ${this.candidateSendQueue.length} candidates that will be sent in answer`); - - this.candidateSendQueue = []; - + const discardCount = this.discardDuplicateCandidates(); + _logger.logger.info(`Call ${this.callId} sendAnswer() discarding ${discardCount} candidates that will be sent in answer`); try { - await this.sendVoipEvent(_event.EventType.CallAnswer, answerContent); // If this isn't the first time we've tried to send the answer, + await this.sendVoipEvent(_event.EventType.CallAnswer, answerContent); + // If this isn't the first time we've tried to send the answer, // we may have candidates queued up, so send them now. - this.inviteOrAnswerSent = true; } catch (error) { // We've failed to answer: back to the ringing state - this.setState(CallState.Ringing); - this.client.cancelPendingEvent(error.event); + this.state = CallState.Ringing; + if (error instanceof _httpApi.MatrixError && error.event) this.client.cancelPendingEvent(error.event); let code = CallErrorCode.SendAnswer; let message = "Failed to send answer"; - - if (error.name == 'UnknownDeviceError') { + if (error.name == "UnknownDeviceError") { code = CallErrorCode.UnknownDevices; message = "Unknown devices present in the room"; } - this.emit(CallEvent.Error, new CallError(code, message, error)); throw error; - } // error handler re-throws so this won't happen on error, but - // we don't want the same error handling on the candidate queue - + } + // error handler re-throws so this won't happen on error, but + // we don't want the same error handling on the candidate queue this.sendCandidateQueue(); } - + queueGotCallFeedsForAnswer(callFeeds) { + // Ensure only one negotiate/answer event is being processed at a time. + if (this.responsePromiseChain) { + this.responsePromiseChain = this.responsePromiseChain.then(() => this.gotCallFeedsForAnswer(callFeeds)); + } else { + this.responsePromiseChain = this.gotCallFeedsForAnswer(callFeeds); + } + } + + // Enables DTX (discontinuous transmission) on the given session to reduce + // bandwidth when transmitting silence + mungeSdp(description, mods) { + // The only way to enable DTX at this time is through SDP munging + const sdp = (0, _sdpTransform.parse)(description.sdp); + sdp.media.forEach(media => { + const payloadTypeToCodecMap = new Map(); + const codecToPayloadTypeMap = new Map(); + for (const rtp of media.rtp) { + payloadTypeToCodecMap.set(rtp.payload, rtp.codec); + codecToPayloadTypeMap.set(rtp.codec, rtp.payload); + } + for (const mod of mods) { + if (mod.mediaType !== media.type) continue; + if (!codecToPayloadTypeMap.has(mod.codec)) { + _logger.logger.info(`Call ${this.callId} mungeSdp() ignoring SDP modifications for ${mod.codec} as it's not present.`); + continue; + } + const extraConfig = []; + if (mod.enableDtx !== undefined) { + extraConfig.push(`usedtx=${mod.enableDtx ? "1" : "0"}`); + } + if (mod.maxAverageBitrate !== undefined) { + extraConfig.push(`maxaveragebitrate=${mod.maxAverageBitrate}`); + } + let found = false; + for (const fmtp of media.fmtp) { + if (payloadTypeToCodecMap.get(fmtp.payload) === mod.codec) { + found = true; + fmtp.config += ";" + extraConfig.join(";"); + } + } + if (!found) { + media.fmtp.push({ + payload: codecToPayloadTypeMap.get(mod.codec), + config: extraConfig.join(";") + }); + } + } + }); + description.sdp = (0, _sdpTransform.write)(sdp); + } + async createOffer() { + const offer = await this.peerConn.createOffer(); + this.mungeSdp(offer, getCodecParamMods(this.isPtt)); + return offer; + } + async createAnswer() { + const answer = await this.peerConn.createAnswer(); + this.mungeSdp(answer, getCodecParamMods(this.isPtt)); + return answer; + } async gotCallFeedsForAnswer(callFeeds) { if (this.callHasEnded()) return; this.waitForLocalAVStream = false; - for (const feed of callFeeds) { this.pushLocalFeed(feed); } - - this.setState(CallState.CreateAnswer); - let myAnswer; - + this.state = CallState.CreateAnswer; + let answer; try { this.getRidOfRTXCodecs(); - myAnswer = await this.peerConn.createAnswer(); + answer = await this.createAnswer(); } catch (err) { - _logger.logger.debug("Failed to create answer: ", err); - + _logger.logger.debug(`Call ${this.callId} gotCallFeedsForAnswer() failed to create answer: `, err); this.terminate(CallParty.Local, CallErrorCode.CreateAnswer, true); return; } - try { - await this.peerConn.setLocalDescription(myAnswer); - this.setState(CallState.Connecting); // Allow a short time for initial candidates to be gathered + await this.peerConn.setLocalDescription(answer); + + // make sure we're still going + if (this.callHasEnded()) return; + this.state = CallState.Connecting; + // Allow a short time for initial candidates to be gathered await new Promise(resolve => { setTimeout(resolve, 200); }); + + // make sure the call hasn't ended before we continue + if (this.callHasEnded()) return; this.sendAnswer(); } catch (err) { - _logger.logger.debug("Error setting local description!", err); - + _logger.logger.debug(`Call ${this.callId} gotCallFeedsForAnswer() error setting local description!`, err); this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true); return; } } + /** * Internal - * @param {Object} event */ - async onRemoteIceCandidatesReceived(ev) { if (this.callHasEnded()) { //debuglog("Ignoring remote ICE candidate because call has ended"); return; } - const content = ev.getContent(); const candidates = content.candidates; - if (!candidates) { - _logger.logger.info("Ignoring candidates event with no candidates!"); - + _logger.logger.info(`Call ${this.callId} onRemoteIceCandidatesReceived() ignoring candidates event with no candidates!`); return; } - const fromPartyId = content.version === 0 ? null : content.party_id || null; - if (this.opponentPartyId === undefined) { // we haven't picked an opponent yet so save the candidates - _logger.logger.info(`Buffering ${candidates.length} candidates until we pick an opponent`); - - const bufferedCandidates = this.remoteCandidateBuffer.get(fromPartyId) || []; - bufferedCandidates.push(...candidates); - this.remoteCandidateBuffer.set(fromPartyId, bufferedCandidates); + if (fromPartyId) { + _logger.logger.info(`Call ${this.callId} onRemoteIceCandidatesReceived() buffering ${candidates.length} candidates until we pick an opponent`); + const bufferedCandidates = this.remoteCandidateBuffer.get(fromPartyId) || []; + bufferedCandidates.push(...candidates); + this.remoteCandidateBuffer.set(fromPartyId, bufferedCandidates); + } return; } - if (!this.partyIdMatches(content)) { - _logger.logger.info(`Ignoring candidates from party ID ${content.party_id}: ` + `we have chosen party ID ${this.opponentPartyId}`); - + _logger.logger.info(`Call ${this.callId} onRemoteIceCandidatesReceived() ignoring candidates from party ID ${content.party_id}: we have chosen party ID ${this.opponentPartyId}`); return; } - await this.addIceCandidates(candidates); } + /** * Used by MatrixClient. - * @param {Object} msg */ - - async onAnswerReceived(event) { const content = event.getContent(); - - _logger.logger.debug(`Got answer for call ID ${this.callId} from party ID ${content.party_id}`); - + _logger.logger.debug(`Call ${this.callId} onAnswerReceived() running (hangupParty=${content.party_id})`); if (this.callHasEnded()) { - _logger.logger.debug(`Ignoring answer because call ID ${this.callId} has ended`); - + _logger.logger.debug(`Call ${this.callId} onAnswerReceived() ignoring answer because call has ended`); return; } - if (this.opponentPartyId !== undefined) { - _logger.logger.info(`Ignoring answer from party ID ${content.party_id}: ` + `we already have an answer/reject from ${this.opponentPartyId}`); - + _logger.logger.info(`Call ${this.callId} onAnswerReceived() ignoring answer from party ID ${content.party_id}: we already have an answer/reject from ${this.opponentPartyId}`); return; } - this.chooseOpponent(event); await this.addBufferedIceCandidates(); - this.setState(CallState.Connecting); + this.state = CallState.Connecting; const sdpStreamMetadata = content[_callEventTypes.SDPStreamMetadataKey]; - if (sdpStreamMetadata) { this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); } else { - _logger.logger.warn("Did not get any SDPStreamMetadata! Can not send/receive multiple streams"); + _logger.logger.warn(`Call ${this.callId} onAnswerReceived() did not get any SDPStreamMetadata! Can not send/receive multiple streams`); } - try { await this.peerConn.setRemoteDescription(content.answer); } catch (e) { - _logger.logger.debug("Failed to set remote description", e); - + _logger.logger.debug(`Call ${this.callId} onAnswerReceived() failed to set remote description`, e); this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false); return; - } // If the answer we selected has a party_id, send a select_answer event + } + + // If the answer we selected has a party_id, send a select_answer event // We do this after setting the remote description since otherwise we'd block // call setup on it - - if (this.opponentPartyId !== null) { try { await this.sendVoipEvent(_event.EventType.CallSelectAnswer, { @@ -1606,96 +1575,83 @@ } catch (err) { // This isn't fatal, and will just mean that if another party has raced to answer // the call, they won't know they got rejected, so we carry on & don't retry. - _logger.logger.warn("Failed to send select_answer event", err); + _logger.logger.warn(`Call ${this.callId} onAnswerReceived() failed to send select_answer event`, err); } } } - async onSelectAnswerReceived(event) { if (this.direction !== CallDirection.Inbound) { - _logger.logger.warn("Got select_answer for an outbound call: ignoring"); - + _logger.logger.warn(`Call ${this.callId} onSelectAnswerReceived() got select_answer for an outbound call: ignoring`); return; } - const selectedPartyId = event.getContent().selected_party_id; - if (selectedPartyId === undefined || selectedPartyId === null) { - _logger.logger.warn("Got nonsensical select_answer with null/undefined selected_party_id: ignoring"); - + _logger.logger.warn(`Call ${this.callId} onSelectAnswerReceived() got nonsensical select_answer with null/undefined selected_party_id: ignoring`); return; } - if (selectedPartyId !== this.ourPartyId) { - _logger.logger.info(`Got select_answer for party ID ${selectedPartyId}: we are party ID ${this.ourPartyId}.`); // The other party has picked somebody else's answer - - + _logger.logger.info(`Call ${this.callId} onSelectAnswerReceived() got select_answer for party ID ${selectedPartyId}: we are party ID ${this.ourPartyId}.`); + // The other party has picked somebody else's answer await this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true); } } - async onNegotiateReceived(event) { const content = event.getContent(); const description = content.description; - if (!description || !description.sdp || !description.type) { - _logger.logger.info("Ignoring invalid m.call.negotiate event"); - + _logger.logger.info(`Call ${this.callId} onNegotiateReceived() ignoring invalid m.call.negotiate event`); return; - } // Politeness always follows the direction of the call: in a glare situation, + } + // Politeness always follows the direction of the call: in a glare situation, // we pick either the inbound or outbound call, so one side will always be // inbound and one outbound + const polite = this.direction === CallDirection.Inbound; - - const polite = this.direction === CallDirection.Inbound; // Here we follow the perfect negotiation logic from + // Here we follow the perfect negotiation logic from // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation - - const offerCollision = description.type === 'offer' && (this.makingOffer || this.peerConn.signalingState !== 'stable'); + const offerCollision = description.type === "offer" && (this.makingOffer || this.peerConn.signalingState !== "stable"); this.ignoreOffer = !polite && offerCollision; - if (this.ignoreOffer) { - _logger.logger.info("Ignoring colliding negotiate event because we're impolite"); - + _logger.logger.info(`Call ${this.callId} onNegotiateReceived() ignoring colliding negotiate event because we're impolite`); return; } - const prevLocalOnHold = this.isLocalOnHold(); const sdpStreamMetadata = content[_callEventTypes.SDPStreamMetadataKey]; - if (sdpStreamMetadata) { this.updateRemoteSDPStreamMetadata(sdpStreamMetadata); } else { - _logger.logger.warn("Received negotiation event without SDPStreamMetadata!"); + _logger.logger.warn(`Call ${this.callId} onNegotiateReceived() received negotiation event without SDPStreamMetadata!`); } - try { await this.peerConn.setRemoteDescription(description); - - if (description.type === 'offer') { - this.getRidOfRTXCodecs(); - const localDescription = await this.peerConn.createAnswer(); - await this.peerConn.setLocalDescription(localDescription); + if (description.type === "offer") { + let answer; + try { + this.getRidOfRTXCodecs(); + answer = await this.createAnswer(); + } catch (err) { + _logger.logger.debug(`Call ${this.callId} onNegotiateReceived() failed to create answer: `, err); + this.terminate(CallParty.Local, CallErrorCode.CreateAnswer, true); + return; + } + await this.peerConn.setLocalDescription(answer); this.sendVoipEvent(_event.EventType.CallNegotiate, { - description: this.peerConn.localDescription, - [_callEventTypes.SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata() + description: this.peerConn.localDescription?.toJSON(), + [_callEventTypes.SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(true) }); } } catch (err) { - _logger.logger.warn("Failed to complete negotiation", err); + _logger.logger.warn(`Call ${this.callId} onNegotiateReceived() failed to complete negotiation`, err); } - const newLocalOnHold = this.isLocalOnHold(); - if (prevLocalOnHold !== newLocalOnHold) { - this.emit(CallEvent.LocalHoldUnhold, newLocalOnHold); // also this one for backwards compat - + this.emit(CallEvent.LocalHoldUnhold, newLocalOnHold); + // also this one for backwards compat this.emit(CallEvent.HoldUnhold, newLocalOnHold); } } - updateRemoteSDPStreamMetadata(metadata) { this.remoteSDPStreamMetadata = utils.recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true); - for (const feed of this.getRemoteFeeds()) { const streamId = feed.stream.id; const metadata = this.remoteSDPStreamMetadata[streamId]; @@ -1703,13 +1659,11 @@ feed.purpose = this.remoteSDPStreamMetadata[streamId]?.purpose; } } - onSDPStreamMetadataChangedReceived(event) { const content = event.getContent(); const metadata = content[_callEventTypes.SDPStreamMetadataKey]; this.updateRemoteSDPStreamMetadata(metadata); } - async onAssertedIdentityReceived(event) { const content = event.getContent(); if (!content.asserted_identity) return; @@ -1719,14 +1673,118 @@ }; this.emit(CallEvent.AssertedIdentityChanged); } - callHasEnded() { // This exists as workaround to typescript trying to be clever and erroring // when putting if (this.state === CallState.Ended) return; twice in the same // function, even though that function is async. return this.state === CallState.Ended; } + queueGotLocalOffer() { + // Ensure only one negotiate/answer event is being processed at a time. + if (this.responsePromiseChain) { + this.responsePromiseChain = this.responsePromiseChain.then(() => this.wrappedGotLocalOffer()); + } else { + this.responsePromiseChain = this.wrappedGotLocalOffer(); + } + } + async wrappedGotLocalOffer() { + this.makingOffer = true; + try { + await this.gotLocalOffer(); + } catch (e) { + this.getLocalOfferFailed(e); + return; + } finally { + this.makingOffer = false; + } + } + async gotLocalOffer() { + _logger.logger.debug(`Call ${this.callId} gotLocalOffer() running`); + if (this.callHasEnded()) { + _logger.logger.debug(`Call ${this.callId} gotLocalOffer() ignoring newly created offer because the call has ended"`); + return; + } + let offer; + try { + this.getRidOfRTXCodecs(); + offer = await this.createOffer(); + } catch (err) { + _logger.logger.debug(`Call ${this.callId} gotLocalOffer() failed to create offer: `, err); + this.terminate(CallParty.Local, CallErrorCode.CreateOffer, true); + return; + } + try { + await this.peerConn.setLocalDescription(offer); + } catch (err) { + _logger.logger.debug(`Call ${this.callId} gotLocalOffer() error setting local description!`, err); + this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true); + return; + } + if (this.peerConn.iceGatheringState === "gathering") { + // Allow a short time for initial candidates to be gathered + await new Promise(resolve => { + setTimeout(resolve, 200); + }); + } + if (this.callHasEnded()) return; + const eventType = this.state === CallState.CreateOffer ? _event.EventType.CallInvite : _event.EventType.CallNegotiate; + const content = { + lifetime: CALL_TIMEOUT_MS + }; + if (eventType === _event.EventType.CallInvite && this.invitee) { + content.invitee = this.invitee; + } + + // clunky because TypeScript can't follow the types through if we use an expression as the key + if (this.state === CallState.CreateOffer) { + content.offer = this.peerConn.localDescription?.toJSON(); + } else { + content.description = this.peerConn.localDescription?.toJSON(); + } + content.capabilities = { + "m.call.transferee": this.client.supportsCallTransfer, + "m.call.dtmf": false + }; + content[_callEventTypes.SDPStreamMetadataKey] = this.getLocalSDPStreamMetadata(true); + + // Get rid of any candidates waiting to be sent: they'll be included in the local + // description we just got and will send in the offer. + const discardCount = this.discardDuplicateCandidates(); + _logger.logger.info(`Call ${this.callId} gotLocalOffer() discarding ${discardCount} candidates that will be sent in offer`); + try { + await this.sendVoipEvent(eventType, content); + } catch (error) { + _logger.logger.error(`Call ${this.callId} gotLocalOffer() failed to send invite`, error); + if (error instanceof _httpApi.MatrixError && error.event) this.client.cancelPendingEvent(error.event); + let code = CallErrorCode.SignallingFailed; + let message = "Signalling failed"; + if (this.state === CallState.CreateOffer) { + code = CallErrorCode.SendInvite; + message = "Failed to send invite"; + } + if (error.name == "UnknownDeviceError") { + code = CallErrorCode.UnknownDevices; + message = "Unknown devices present in the room"; + } + this.emit(CallEvent.Error, new CallError(code, message, error)); + this.terminate(CallParty.Local, code, false); + // no need to carry on & send the candidate queue, but we also + // don't want to rethrow the error + return; + } + this.sendCandidateQueue(); + if (this.state === CallState.CreateOffer) { + this.inviteOrAnswerSent = true; + this.state = CallState.InviteSent; + this.inviteTimeout = setTimeout(() => { + this.inviteTimeout = undefined; + if (this.state === CallState.InviteSent) { + this.hangup(CallErrorCode.InviteTimeout, false); + } + }, CALL_TIMEOUT_MS); + } + } /** * This method removes all video/rtx codecs from screensharing video * transceivers. This is necessary since they can cause problems. Without @@ -1746,74 +1804,129 @@ const recvCodecs = RTCRtpReceiver.getCapabilities("video").codecs; const sendCodecs = RTCRtpSender.getCapabilities("video").codecs; const codecs = [...sendCodecs, ...recvCodecs]; - for (const codec of codecs) { if (codec.mimeType === "video/rtx") { const rtxCodecIndex = codecs.indexOf(codec); codecs.splice(rtxCodecIndex, 1); } } - - for (const trans of this.peerConn.getTransceivers()) { - if (this.screensharingSenders.includes(trans.sender) && (trans.sender.track?.kind === "video" || trans.receiver.track?.kind === "video")) { - trans.setCodecPreferences(codecs); - } - } - } - - setState(state) { - const oldState = this.state; - this.state = state; - this.emit(CallEvent.State, state, oldState); + const screenshareVideoTransceiver = this.transceivers.get(getTransceiverKey(_callEventTypes.SDPStreamMetadataPurpose.Screenshare, "video")); + if (screenshareVideoTransceiver) screenshareVideoTransceiver.setCodecPreferences(codecs); } /** - * Internal - * @param {string} eventType - * @param {Object} content - * @return {Promise} + * @internal */ - - - sendVoipEvent(eventType, content) { - return this.client.sendEvent(this.roomId, eventType, Object.assign({}, content, { + async sendVoipEvent(eventType, content) { + const realContent = Object.assign({}, content, { version: VOIP_PROTO_VERSION, call_id: this.callId, - party_id: this.ourPartyId - })); + party_id: this.ourPartyId, + conf_id: this.groupCallId + }); + if (this.opponentDeviceId) { + const toDeviceSeq = this.toDeviceSeq++; + const content = _objectSpread(_objectSpread({}, realContent), {}, { + device_id: this.client.deviceId, + sender_session_id: this.client.getSessionId(), + dest_session_id: this.opponentSessionId, + seq: toDeviceSeq, + [_event.ToDeviceMessageId]: (0, _uuid.v4)() + }); + this.emit(CallEvent.SendVoipEvent, { + type: "toDevice", + eventType, + userId: this.invitee || this.getOpponentMember()?.userId, + opponentDeviceId: this.opponentDeviceId, + content + }); + const userId = this.invitee || this.getOpponentMember().userId; + if (this.client.getUseE2eForGroupCall()) { + if (!this.opponentDeviceInfo) { + _logger.logger.warn(`Call ${this.callId} sendVoipEvent() failed: we do not have opponentDeviceInfo`); + return; + } + await this.client.encryptAndSendToDevices([{ + userId, + deviceInfo: this.opponentDeviceInfo + }], { + type: eventType, + content + }); + } else { + await this.client.sendToDevice(eventType, new Map([[userId, new Map([[this.opponentDeviceId, content]])]])); + } + } else { + this.emit(CallEvent.SendVoipEvent, { + type: "sendEvent", + eventType, + roomId: this.roomId, + content: realContent, + userId: this.invitee || this.getOpponentMember()?.userId + }); + await this.client.sendEvent(this.roomId, eventType, realContent); + } } + /** + * Queue a candidate to be sent + * @param content - The candidate to queue up, or null if candidates have finished being generated + * and end-of-candidates should be signalled + */ queueCandidate(content) { // We partially de-trickle candidates by waiting for `delay` before sending them // amalgamated, in order to avoid sending too many m.call.candidates events and hitting // rate limits in Matrix. // In practice, it'd be better to remove rate limits for m.call.* + // N.B. this deliberately lets you queue and send blank candidates, which MSC2746 // currently proposes as the way to indicate that candidate gathering is complete. // This will hopefully be changed to an explicit rather than implicit notification // shortly. - this.candidateSendQueue.push(content); // Don't send the ICE candidates yet if the call is in the ringing state: this + if (content) { + this.candidateSendQueue.push(content); + } else { + this.candidatesEnded = true; + } + + // Don't send the ICE candidates yet if the call is in the ringing state: this // means we tried to pick (ie. started generating candidates) and then failed to // send the answer and went back to the ringing state. Queue up the candidates // to send if we successfully send the answer. // Equally don't send if we haven't yet sent the answer because we can send the // first batch of candidates along with the answer + if (this.state === CallState.Ringing || !this.inviteOrAnswerSent) return; - if (this.state === CallState.Ringing || !this.inviteOrAnswerSent) return; // MSC2746 recommends these values (can be quite long when calling because the + // MSC2746 recommends these values (can be quite long when calling because the // callee will need a while to answer the call) - const delay = this.direction === CallDirection.Inbound ? 500 : 2000; - if (this.candidateSendTries === 0) { setTimeout(() => { this.sendCandidateQueue(); }, delay); } } + + // Discard all non-end-of-candidates messages + // Return the number of candidate messages that were discarded. + // Call this method before sending an invite or answer message + discardDuplicateCandidates() { + let discardCount = 0; + const newQueue = []; + for (let i = 0; i < this.candidateSendQueue.length; i++) { + const candidate = this.candidateSendQueue[i]; + if (candidate.candidate === "") { + newQueue.push(candidate); + } else { + discardCount++; + } + } + this.candidateSendQueue = newQueue; + return discardCount; + } + /* * Transfers this call to another user */ - - async transfer(targetUserId) { // Fetch the target user's global profile info: their room avatar / displayname // could be different in whatever room we share with them. @@ -1829,26 +1942,27 @@ create_call: replacementId }; await this.sendVoipEvent(_event.EventType.CallReplaces, body); - await this.terminate(CallParty.Local, CallErrorCode.Transfered, true); + await this.terminate(CallParty.Local, CallErrorCode.Transferred, true); } + /* * Transfers this call to the target call, effectively 'joining' the * two calls (so the remote parties on each call are connected together). */ - - async transferToCall(transferTargetCall) { - const targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId); - const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId); + const targetUserId = transferTargetCall.getOpponentMember()?.userId; + const targetProfileInfo = targetUserId ? await this.client.getProfileInfo(targetUserId) : undefined; + const opponentUserId = this.getOpponentMember()?.userId; + const transfereeProfileInfo = opponentUserId ? await this.client.getProfileInfo(opponentUserId) : undefined; const newCallId = genCallID(); const bodyToTransferTarget = { // the replacements on each side have their own ID, and it's distinct from the // ID of the new call (but we can use the same function to generate it) replacement_id: genCallID(), target_user: { - id: this.getOpponentMember().userId, - display_name: transfereeProfileInfo.displayname, - avatar_url: transfereeProfileInfo.avatar_url + id: opponentUserId, + display_name: transfereeProfileInfo?.displayname, + avatar_url: transfereeProfileInfo?.avatar_url }, await_call: newCallId }; @@ -1856,142 +1970,151 @@ const bodyToTransferee = { replacement_id: genCallID(), target_user: { - id: transferTargetCall.getOpponentMember().userId, - display_name: targetProfileInfo.displayname, - avatar_url: targetProfileInfo.avatar_url + id: targetUserId, + display_name: targetProfileInfo?.displayname, + avatar_url: targetProfileInfo?.avatar_url }, create_call: newCallId }; await this.sendVoipEvent(_event.EventType.CallReplaces, bodyToTransferee); - await this.terminate(CallParty.Local, CallErrorCode.Transfered, true); - await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transfered, true); + await this.terminate(CallParty.Local, CallErrorCode.Transferred, true); + await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Transferred, true); } - async terminate(hangupParty, hangupReason, shouldEmit) { if (this.callHasEnded()) return; - this.callStatsAtEnd = await this.collectCallStats(); - + this.hangupParty = hangupParty; + this.hangupReason = hangupReason; + this.state = CallState.Ended; if (this.inviteTimeout) { clearTimeout(this.inviteTimeout); - this.inviteTimeout = null; + this.inviteTimeout = undefined; + } + if (this.iceDisconnectedTimeout !== undefined) { + clearTimeout(this.iceDisconnectedTimeout); + this.iceDisconnectedTimeout = undefined; } - if (this.callLengthInterval) { clearInterval(this.callLengthInterval); - this.callLengthInterval = null; - } // Order is important here: first we stopAllMedia() and only then we can deleteAllFeeds() - // We don't stop media if the call was replaced as we want to re-use streams in the successor - + this.callLengthInterval = undefined; + } + if (this.stopVideoTrackTimer !== undefined) { + clearTimeout(this.stopVideoTrackTimer); + this.stopVideoTrackTimer = undefined; + } + for (const [stream, listener] of this.removeTrackListeners) { + stream.removeEventListener("removetrack", listener); + } + this.removeTrackListeners.clear(); + this.callStatsAtEnd = await this.collectCallStats(); - if (hangupReason !== CallErrorCode.Replaced) this.stopAllMedia(); + // Order is important here: first we stopAllMedia() and only then we can deleteAllFeeds() + this.stopAllMedia(); this.deleteAllFeeds(); - this.hangupParty = hangupParty; - this.hangupReason = hangupReason; - this.setState(CallState.Ended); - - if (this.peerConn && this.peerConn.signalingState !== 'closed') { + if (this.peerConn && this.peerConn.signalingState !== "closed") { this.peerConn.close(); } - if (shouldEmit) { - this.emit(CallEvent.Hangup); + this.emit(CallEvent.Hangup, this); } + this.client.callEventHandler.calls.delete(this.callId); } - stopAllMedia() { - _logger.logger.debug("Stopping all media for call", this.callId); - + _logger.logger.debug(`Call ${this.callId} stopAllMedia() running`); for (const feed of this.feeds) { + // Slightly awkward as local feed need to go via the correct method on + // the MediaHandler so they get removed from MediaHandler (remote tracks + // don't) + // NB. We clone local streams when passing them to individual calls in a group + // call, so we can (and should) stop the clones once we no longer need them: + // the other clones will continue fine. if (feed.isLocal() && feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Usermedia) { this.client.getMediaHandler().stopUserMediaStream(feed.stream); } else if (feed.isLocal() && feed.purpose === _callEventTypes.SDPStreamMetadataPurpose.Screenshare) { this.client.getMediaHandler().stopScreensharingStream(feed.stream); - } else { - _logger.logger.debug("Stopping remote stream", feed.stream.id); - + } else if (!feed.isLocal()) { + _logger.logger.debug(`Call ${this.callId} stopAllMedia() stopping stream (streamId=${feed.stream.id})`); for (const track of feed.stream.getTracks()) { track.stop(); } } } } - checkForErrorListener() { if (this.listeners(_typedEventEmitter.EventEmitterEvents.Error).length === 0) { throw new Error("You MUST attach an error listener using call.on('error', function() {})"); } } - async sendCandidateQueue() { - if (this.candidateSendQueue.length === 0) { + if (this.candidateSendQueue.length === 0 || this.callHasEnded()) { return; } - const candidates = this.candidateSendQueue; this.candidateSendQueue = []; ++this.candidateSendTries; const content = { - candidates: candidates + candidates: candidates.map(candidate => candidate.toJSON()) }; - - _logger.logger.debug("Attempting to send " + candidates.length + " candidates"); - + if (this.candidatesEnded) { + // If there are no more candidates, signal this by adding an empty string candidate + content.candidates.push({ + candidate: "" + }); + } + _logger.logger.debug(`Call ${this.callId} sendCandidateQueue() attempting to send ${candidates.length} candidates`); try { - await this.sendVoipEvent(_event.EventType.CallCandidates, content); // reset our retry count if we have successfully sent our candidates + await this.sendVoipEvent(_event.EventType.CallCandidates, content); + // reset our retry count if we have successfully sent our candidates // otherwise queueCandidate() will refuse to try to flush the queue - this.candidateSendTries = 0; + + // Try to send candidates again just in case we received more candidates while sending. + this.sendCandidateQueue(); } catch (error) { // don't retry this event: we'll send another one later as we might // have more candidates by then. - if (error.event) this.client.cancelPendingEvent(error.event); // put all the candidates we failed to send back in the queue + if (error instanceof _httpApi.MatrixError && error.event) this.client.cancelPendingEvent(error.event); + // put all the candidates we failed to send back in the queue this.candidateSendQueue.push(...candidates); - if (this.candidateSendTries > 5) { - _logger.logger.debug("Failed to send candidates on attempt " + this.candidateSendTries + ". Giving up on this call.", error); - + _logger.logger.debug(`Call ${this.callId} sendCandidateQueue() failed to send candidates on attempt ${this.candidateSendTries}. Giving up on this call.`, error); const code = CallErrorCode.SignallingFailed; const message = "Signalling failed"; this.emit(CallEvent.Error, new CallError(code, message, error)); this.hangup(code, false); return; } - const delayMs = 500 * Math.pow(2, this.candidateSendTries); ++this.candidateSendTries; - - _logger.logger.debug("Failed to send candidates. Retrying in " + delayMs + "ms", error); - + _logger.logger.debug(`Call ${this.callId} sendCandidateQueue() failed to send candidates. Retrying in ${delayMs}ms`, error); setTimeout(() => { this.sendCandidateQueue(); }, delayMs); } } + /** * Place a call to this room. * @throws if you have not specified a listener for 'error' events. * @throws if have passed audio=false. */ - - async placeCall(audio, video) { if (!audio) { throw new Error("You CANNOT start a call without audio"); } - - this.setState(CallState.WaitLocalMedia); - + this.state = CallState.WaitLocalMedia; try { - const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video); // make sure all the tracks are enabled (same as pushNewLocalFeed - - // we probably ought to just have one code path for adding streams) + const stream = await this.client.getMediaHandler().getUserMediaStream(audio, video); + // make sure all the tracks are enabled (same as pushNewLocalFeed - + // we probably ought to just have one code path for adding streams) setTracksEnabled(stream.getAudioTracks(), true); setTracksEnabled(stream.getVideoTracks(), true); const callFeed = new _callFeed.CallFeed({ client: this.client, roomId: this.roomId, userId: this.client.getUserId(), + deviceId: this.client.getDeviceId() ?? undefined, stream, purpose: _callEventTypes.SDPStreamMetadataPurpose.Usermedia, audioMuted: false, @@ -2003,50 +2126,51 @@ return; } } + /** * Place a call to this room with call feed. - * @param {CallFeed[]} callFeeds to use + * @param callFeeds - to use * @throws if you have not specified a listener for 'error' events. * @throws if have passed audio=false. */ - - - async placeCallWithCallFeeds(callFeeds) { + async placeCallWithCallFeeds(callFeeds, requestScreenshareFeed = false) { this.checkForErrorListener(); - this.direction = CallDirection.Outbound; // XXX Find a better way to do this + this.direction = CallDirection.Outbound; + await this.initOpponentCrypto(); - this.client.callEventHandler.calls.set(this.callId, this); // make sure we have valid turn creds. Unless something's gone wrong, it should - // poll and keep the credentials valid so this should be instant. + // XXX Find a better way to do this + this.client.callEventHandler.calls.set(this.callId, this); + // make sure we have valid turn creds. Unless something's gone wrong, it should + // poll and keep the credentials valid so this should be instant. const haveTurnCreds = await this.client.checkTurnServers(); - if (!haveTurnCreds) { - _logger.logger.warn("Failed to get TURN credentials! Proceeding with call anyway..."); - } // create the peer connection now so it can be gathering candidates while we get user - // media (assuming a candidate pool size is configured) - + _logger.logger.warn(`Call ${this.callId} placeCallWithCallFeeds() failed to get TURN credentials! Proceeding with call anyway...`); + } + // create the peer connection now so it can be gathering candidates while we get user + // media (assuming a candidate pool size is configured) this.peerConn = this.createPeerConnection(); - this.gotCallFeedsForInvite(callFeeds); + this.gotCallFeedsForInvite(callFeeds, requestScreenshareFeed); } - createPeerConnection() { const pc = new window.RTCPeerConnection({ - iceTransportPolicy: this.forceTURN ? 'relay' : undefined, + iceTransportPolicy: this.forceTURN ? "relay" : undefined, iceServers: this.turnServers, - iceCandidatePoolSize: this.client.iceCandidatePoolSize - }); // 'connectionstatechange' would be better, but firefox doesn't implement that. + iceCandidatePoolSize: this.client.iceCandidatePoolSize, + bundlePolicy: "max-bundle" + }); - pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChanged); - pc.addEventListener('signalingstatechange', this.onSignallingStateChanged); - pc.addEventListener('icecandidate', this.gotLocalIceCandidate); - pc.addEventListener('icegatheringstatechange', this.onIceGatheringStateChange); - pc.addEventListener('track', this.onTrack); - pc.addEventListener('negotiationneeded', this.onNegotiationNeeded); - pc.addEventListener('datachannel', this.onDataChannel); + // 'connectionstatechange' would be better, but firefox doesn't implement that. + pc.addEventListener("iceconnectionstatechange", this.onIceConnectionStateChanged); + pc.addEventListener("signalingstatechange", this.onSignallingStateChanged); + pc.addEventListener("icecandidate", this.gotLocalIceCandidate); + pc.addEventListener("icegatheringstatechange", this.onIceGatheringStateChange); + pc.addEventListener("track", this.onTrack); + pc.addEventListener("negotiationneeded", this.onNegotiationNeeded); + pc.addEventListener("datachannel", this.onDataChannel); return pc; } - partyIdMatches(msg) { // They must either match or both be absent (in which case opponentPartyId will be null) // Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same @@ -2054,18 +2178,15 @@ // same call with different versions) const msgPartyId = msg.version === 0 ? null : msg.party_id || null; return msgPartyId === this.opponentPartyId; - } // Commits to an opponent for the call - // ev: An invite or answer event - + } + // Commits to an opponent for the call + // ev: An invite or answer event chooseOpponent(ev) { // I choo-choo-choose you const msg = ev.getContent(); - - _logger.logger.debug(`Choosing party ID ${msg.party_id} for call ID ${this.callId}`); - + _logger.logger.debug(`Call ${this.callId} chooseOpponent() running (partyId=${msg.party_id})`); this.opponentVersion = msg.version; - if (this.opponentVersion === 0) { // set to null to indicate that we've chosen an opponent, but because // they're v0 they have no party ID (even if they sent one, we're ignoring it) @@ -2076,112 +2197,93 @@ // party ID this.opponentPartyId = msg.party_id || null; } - this.opponentCaps = msg.capabilities || {}; - this.opponentMember = ev.sender; + this.opponentMember = this.client.getRoom(this.roomId).getMember(ev.getSender()) ?? undefined; } - async addBufferedIceCandidates() { const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId); - if (bufferedCandidates) { - _logger.logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`); - + _logger.logger.info(`Call ${this.callId} addBufferedIceCandidates() adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`); await this.addIceCandidates(bufferedCandidates); } - - this.remoteCandidateBuffer = null; + this.remoteCandidateBuffer.clear(); } - async addIceCandidates(candidates) { for (const candidate of candidates) { if ((candidate.sdpMid === null || candidate.sdpMid === undefined) && (candidate.sdpMLineIndex === null || candidate.sdpMLineIndex === undefined)) { - _logger.logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex"); - - continue; + _logger.logger.debug(`Call ${this.callId} addIceCandidates() got remote ICE end-of-candidates`); + } else { + _logger.logger.debug(`Call ${this.callId} addIceCandidates() got remote ICE candidate (sdpMid=${candidate.sdpMid}, candidate=${candidate.candidate})`); } - - _logger.logger.debug("Call " + this.callId + " got remote ICE " + candidate.sdpMid + " candidate: " + candidate.candidate); - try { await this.peerConn.addIceCandidate(candidate); } catch (err) { if (!this.ignoreOffer) { - _logger.logger.info("Failed to add remote ICE candidate", err); + _logger.logger.info(`Call ${this.callId} addIceCandidates() failed to add remote ICE candidate`, err); } } } } - get hasPeerConnection() { return Boolean(this.peerConn); } - } - exports.MatrixCall = MatrixCall; - function setTracksEnabled(tracks, enabled) { - for (let i = 0; i < tracks.length; i++) { - tracks[i].enabled = enabled; + for (const track of tracks) { + track.enabled = enabled; } } - function supportsMatrixCall() { // typeof prevents Node from erroring on an undefined reference - if (typeof window === 'undefined' || typeof document === 'undefined') { + if (typeof window === "undefined" || typeof document === "undefined") { // NB. We don't log here as apps try to create a call object as a test for // whether calls are supported, so we shouldn't fill the logs up. return false; - } // Firefox throws on so little as accessing the RTCPeerConnection when operating in a secure mode. + } + + // Firefox throws on so little as accessing the RTCPeerConnection when operating in a secure mode. // There's some information at https://bugzilla.mozilla.org/show_bug.cgi?id=1542616 though the concern // is that the browser throwing a SecurityError will brick the client creation process. - - try { const supported = Boolean(window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate || navigator.mediaDevices); - if (!supported) { - /* istanbul ignore if */ - // Adds a lot of noise to test runs, so disable logging there. + /* istanbul ignore if */ // Adds a lot of noise to test runs, so disable logging there. if (process.env.NODE_ENV !== "test") { _logger.logger.error("WebRTC is not supported in this browser / environment"); } - return false; } } catch (e) { _logger.logger.error("Exception thrown when trying to access WebRTC", e); - return false; } - return true; } + /** * DEPRECATED * Use client.createCall() * * Create a new Matrix call for the browser. - * @param {MatrixClient} client The client instance to use. - * @param {string} roomId The room the call is in. - * @param {Object?} options DEPRECATED optional options map. - * @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be - * forced. This option is deprecated - use opts.forceTURN when creating the matrix client - * since it's only possible to set this option on outbound calls. - * @return {MatrixCall} the call or null if the browser doesn't support calling. + * @param client - The client instance to use. + * @param roomId - The room the call is in. + * @param options - DEPRECATED optional options map. + * @returns the call or null if the browser doesn't support calling. */ - - function createNewMatrixCall(client, roomId, options) { if (!supportsMatrixCall()) return null; const optionsForceTURN = options ? options.forceTURN : false; const opts = { client: client, roomId: roomId, + invitee: options?.invitee, turnServers: client.getTurnServers(), // call level options - forceTURN: client.forceTURN || optionsForceTURN + forceTURN: client.forceTURN || optionsForceTURN, + opponentDeviceId: options?.opponentDeviceId, + opponentSessionId: options?.opponentSessionId, + groupCallId: options?.groupCallId }; const call = new MatrixCall(opts); client.reEmitter.reEmit(call, Object.values(CallEvent)); diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/groupCallEventHandler.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/groupCallEventHandler.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/groupCallEventHandler.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/groupCallEventHandler.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,169 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.GroupCallEventHandlerEvent = exports.GroupCallEventHandler = void 0; +var _client = require("../client"); +var _groupCall = require("./groupCall"); +var _roomState = require("../models/room-state"); +var _logger = require("../logger"); +var _event = require("../@types/event"); +var _sync = require("../sync"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +let GroupCallEventHandlerEvent; +exports.GroupCallEventHandlerEvent = GroupCallEventHandlerEvent; +(function (GroupCallEventHandlerEvent) { + GroupCallEventHandlerEvent["Incoming"] = "GroupCall.incoming"; + GroupCallEventHandlerEvent["Outgoing"] = "GroupCall.outgoing"; + GroupCallEventHandlerEvent["Ended"] = "GroupCall.ended"; + GroupCallEventHandlerEvent["Participants"] = "GroupCall.participants"; +})(GroupCallEventHandlerEvent || (exports.GroupCallEventHandlerEvent = GroupCallEventHandlerEvent = {})); +class GroupCallEventHandler { + // roomId -> GroupCall + + // All rooms we know about and whether we've seen a 'Room' event + // for them. The promise will be fulfilled once we've processed that + // event which means we're "up to date" on what calls are in a room + // and get + + constructor(client) { + this.client = client; + _defineProperty(this, "groupCalls", new Map()); + _defineProperty(this, "roomDeferreds", new Map()); + _defineProperty(this, "onRoomsChanged", room => { + this.createGroupCallForRoom(room); + }); + _defineProperty(this, "onRoomStateChanged", (event, state) => { + const eventType = event.getType(); + if (eventType === _event.EventType.GroupCallPrefix) { + const groupCallId = event.getStateKey(); + const content = event.getContent(); + const currentGroupCall = this.groupCalls.get(state.roomId); + if (!currentGroupCall && !content["m.terminated"]) { + this.createGroupCallFromRoomStateEvent(event); + } else if (currentGroupCall && currentGroupCall.groupCallId === groupCallId) { + if (content["m.terminated"]) { + currentGroupCall.terminate(false); + } else if (content["m.type"] !== currentGroupCall.type) { + // TODO: Handle the callType changing when the room state changes + _logger.logger.warn(`GroupCallEventHandler onRoomStateChanged() currently does not support changing type (roomId=${state.roomId})`); + } + } else if (currentGroupCall && currentGroupCall.groupCallId !== groupCallId) { + // TODO: Handle new group calls and multiple group calls + _logger.logger.warn(`GroupCallEventHandler onRoomStateChanged() currently does not support multiple calls (roomId=${state.roomId})`); + } + } + }); + } + async start() { + // We wait until the client has started syncing for real. + // This is because we only support one call at a time, and want + // the latest. We therefore want the latest state of the room before + // we create a group call for the room so we can be fairly sure that + // the group call we create is really the latest one. + if (this.client.getSyncState() !== _sync.SyncState.Syncing) { + _logger.logger.debug("GroupCallEventHandler start() waiting for client to start syncing"); + await new Promise(resolve => { + const onSync = () => { + if (this.client.getSyncState() === _sync.SyncState.Syncing) { + this.client.off(_client.ClientEvent.Sync, onSync); + return resolve(); + } + }; + this.client.on(_client.ClientEvent.Sync, onSync); + }); + } + const rooms = this.client.getRooms(); + for (const room of rooms) { + this.createGroupCallForRoom(room); + } + this.client.on(_client.ClientEvent.Room, this.onRoomsChanged); + this.client.on(_roomState.RoomStateEvent.Events, this.onRoomStateChanged); + } + stop() { + this.client.removeListener(_roomState.RoomStateEvent.Events, this.onRoomStateChanged); + } + getRoomDeferred(roomId) { + let deferred = this.roomDeferreds.get(roomId); + if (deferred === undefined) { + let resolveFunc; + deferred = { + prom: new Promise(resolve => { + resolveFunc = resolve; + }) + }; + deferred.resolve = resolveFunc; + this.roomDeferreds.set(roomId, deferred); + } + return deferred; + } + waitUntilRoomReadyForGroupCalls(roomId) { + return this.getRoomDeferred(roomId).prom; + } + getGroupCallById(groupCallId) { + return [...this.groupCalls.values()].find(groupCall => groupCall.groupCallId === groupCallId); + } + createGroupCallForRoom(room) { + const callEvents = room.currentState.getStateEvents(_event.EventType.GroupCallPrefix); + const sortedCallEvents = callEvents.sort((a, b) => b.getTs() - a.getTs()); + for (const callEvent of sortedCallEvents) { + const content = callEvent.getContent(); + if (content["m.terminated"]) { + continue; + } + _logger.logger.debug(`GroupCallEventHandler createGroupCallForRoom() choosing group call from possible calls (stateKey=${callEvent.getStateKey()}, ts=${callEvent.getTs()}, roomId=${room.roomId}, numOfPossibleCalls=${callEvents.length})`); + this.createGroupCallFromRoomStateEvent(callEvent); + break; + } + _logger.logger.info(`GroupCallEventHandler createGroupCallForRoom() processed room (roomId=${room.roomId})`); + this.getRoomDeferred(room.roomId).resolve(); + } + createGroupCallFromRoomStateEvent(event) { + const roomId = event.getRoomId(); + const content = event.getContent(); + const room = this.client.getRoom(roomId); + if (!room) { + _logger.logger.warn(`GroupCallEventHandler createGroupCallFromRoomStateEvent() couldn't find room for call (roomId=${roomId})`); + return; + } + const groupCallId = event.getStateKey(); + const callType = content["m.type"]; + if (!Object.values(_groupCall.GroupCallType).includes(callType)) { + _logger.logger.warn(`GroupCallEventHandler createGroupCallFromRoomStateEvent() received invalid call type (type=${callType}, roomId=${roomId})`); + return; + } + const callIntent = content["m.intent"]; + if (!Object.values(_groupCall.GroupCallIntent).includes(callIntent)) { + _logger.logger.warn(`Received invalid group call intent (type=${callType}, roomId=${roomId})`); + return; + } + const isPtt = Boolean(content["io.element.ptt"]); + let dataChannelOptions; + if (content?.dataChannelsEnabled && content?.dataChannelOptions) { + // Pull out just the dataChannelOptions we want to support. + const { + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + } = content.dataChannelOptions; + dataChannelOptions = { + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + } + const groupCall = new _groupCall.GroupCall(this.client, room, callType, isPtt, callIntent, groupCallId, + // Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a + // no media WebRTC connection anyway. + content?.dataChannelsEnabled || this.client.isVoipWithNoMediaAllowed, dataChannelOptions, this.client.isVoipWithNoMediaAllowed); + this.groupCalls.set(room.roomId, groupCall); + this.client.emit(GroupCallEventHandlerEvent.Incoming, groupCall); + return groupCall; + } +} +exports.GroupCallEventHandler = GroupCallEventHandler; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/groupCall.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/groupCall.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/groupCall.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/groupCall.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,1101 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.OtherUserSpeakingError = exports.GroupCallUnknownDeviceError = exports.GroupCallType = exports.GroupCallTerminationReason = exports.GroupCallState = exports.GroupCallIntent = exports.GroupCallEvent = exports.GroupCallErrorCode = exports.GroupCallError = exports.GroupCall = void 0; +var _typedEventEmitter = require("../models/typed-event-emitter"); +var _callFeed = require("./callFeed"); +var _call = require("./call"); +var _roomState = require("../models/room-state"); +var _logger = require("../logger"); +var _ReEmitter = require("../ReEmitter"); +var _callEventTypes = require("./callEventTypes"); +var _event = require("../@types/event"); +var _callEventHandler = require("./callEventHandler"); +var _groupCallEventHandler = require("./groupCallEventHandler"); +var _utils = require("../utils"); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +let GroupCallIntent; +exports.GroupCallIntent = GroupCallIntent; +(function (GroupCallIntent) { + GroupCallIntent["Ring"] = "m.ring"; + GroupCallIntent["Prompt"] = "m.prompt"; + GroupCallIntent["Room"] = "m.room"; +})(GroupCallIntent || (exports.GroupCallIntent = GroupCallIntent = {})); +let GroupCallType; +exports.GroupCallType = GroupCallType; +(function (GroupCallType) { + GroupCallType["Video"] = "m.video"; + GroupCallType["Voice"] = "m.voice"; +})(GroupCallType || (exports.GroupCallType = GroupCallType = {})); +let GroupCallTerminationReason; +exports.GroupCallTerminationReason = GroupCallTerminationReason; +(function (GroupCallTerminationReason) { + GroupCallTerminationReason["CallEnded"] = "call_ended"; +})(GroupCallTerminationReason || (exports.GroupCallTerminationReason = GroupCallTerminationReason = {})); +let GroupCallEvent; +exports.GroupCallEvent = GroupCallEvent; +(function (GroupCallEvent) { + GroupCallEvent["GroupCallStateChanged"] = "group_call_state_changed"; + GroupCallEvent["ActiveSpeakerChanged"] = "active_speaker_changed"; + GroupCallEvent["CallsChanged"] = "calls_changed"; + GroupCallEvent["UserMediaFeedsChanged"] = "user_media_feeds_changed"; + GroupCallEvent["ScreenshareFeedsChanged"] = "screenshare_feeds_changed"; + GroupCallEvent["LocalScreenshareStateChanged"] = "local_screenshare_state_changed"; + GroupCallEvent["LocalMuteStateChanged"] = "local_mute_state_changed"; + GroupCallEvent["ParticipantsChanged"] = "participants_changed"; + GroupCallEvent["Error"] = "error"; +})(GroupCallEvent || (exports.GroupCallEvent = GroupCallEvent = {})); +let GroupCallErrorCode; +exports.GroupCallErrorCode = GroupCallErrorCode; +(function (GroupCallErrorCode) { + GroupCallErrorCode["NoUserMedia"] = "no_user_media"; + GroupCallErrorCode["UnknownDevice"] = "unknown_device"; + GroupCallErrorCode["PlaceCallFailed"] = "place_call_failed"; +})(GroupCallErrorCode || (exports.GroupCallErrorCode = GroupCallErrorCode = {})); +class GroupCallError extends Error { + constructor(code, msg, err) { + // Still don't think there's any way to have proper nested errors + if (err) { + super(msg + ": " + err); + _defineProperty(this, "code", void 0); + } else { + super(msg); + _defineProperty(this, "code", void 0); + } + this.code = code; + } +} +exports.GroupCallError = GroupCallError; +class GroupCallUnknownDeviceError extends GroupCallError { + constructor(userId) { + super(GroupCallErrorCode.UnknownDevice, "No device found for " + userId); + this.userId = userId; + } +} +exports.GroupCallUnknownDeviceError = GroupCallUnknownDeviceError; +class OtherUserSpeakingError extends Error { + constructor() { + super("Cannot unmute: another user is speaking"); + } +} +exports.OtherUserSpeakingError = OtherUserSpeakingError; +let GroupCallState; +exports.GroupCallState = GroupCallState; +(function (GroupCallState) { + GroupCallState["LocalCallFeedUninitialized"] = "local_call_feed_uninitialized"; + GroupCallState["InitializingLocalCallFeed"] = "initializing_local_call_feed"; + GroupCallState["LocalCallFeedInitialized"] = "local_call_feed_initialized"; + GroupCallState["Entered"] = "entered"; + GroupCallState["Ended"] = "ended"; +})(GroupCallState || (exports.GroupCallState = GroupCallState = {})); +const DEVICE_TIMEOUT = 1000 * 60 * 60; // 1 hour + +function getCallUserId(call) { + return call.getOpponentMember()?.userId || call.invitee || null; +} +class GroupCall extends _typedEventEmitter.TypedEventEmitter { + // Config + + // user_id -> device_id -> MatrixCall + // user_id -> device_id -> ICallHandlers + + // user_id -> device_id -> count + + constructor(client, room, type, isPtt, intent, groupCallId, dataChannelsEnabled, dataChannelOptions, isCallWithoutVideoAndAudio) { + super(); + this.client = client; + this.room = room; + this.type = type; + this.isPtt = isPtt; + this.intent = intent; + this.dataChannelsEnabled = dataChannelsEnabled; + this.dataChannelOptions = dataChannelOptions; + _defineProperty(this, "activeSpeakerInterval", 1000); + _defineProperty(this, "retryCallInterval", 5000); + _defineProperty(this, "participantTimeout", 1000 * 15); + _defineProperty(this, "pttMaxTransmitTime", 1000 * 20); + _defineProperty(this, "activeSpeaker", void 0); + _defineProperty(this, "localCallFeed", void 0); + _defineProperty(this, "localScreenshareFeed", void 0); + _defineProperty(this, "localDesktopCapturerSourceId", void 0); + _defineProperty(this, "userMediaFeeds", []); + _defineProperty(this, "screenshareFeeds", []); + _defineProperty(this, "groupCallId", void 0); + _defineProperty(this, "allowCallWithoutVideoAndAudio", void 0); + _defineProperty(this, "calls", new Map()); + _defineProperty(this, "callHandlers", new Map()); + _defineProperty(this, "activeSpeakerLoopInterval", void 0); + _defineProperty(this, "retryCallLoopInterval", void 0); + _defineProperty(this, "retryCallCounts", new Map()); + _defineProperty(this, "reEmitter", void 0); + _defineProperty(this, "transmitTimer", null); + _defineProperty(this, "participantsExpirationTimer", null); + _defineProperty(this, "resendMemberStateTimer", null); + _defineProperty(this, "initWithAudioMuted", false); + _defineProperty(this, "initWithVideoMuted", false); + _defineProperty(this, "initCallFeedPromise", void 0); + _defineProperty(this, "_state", GroupCallState.LocalCallFeedUninitialized); + _defineProperty(this, "_participants", new Map()); + _defineProperty(this, "_creationTs", null); + _defineProperty(this, "_enteredViaAnotherSession", false); + _defineProperty(this, "onIncomingCall", newCall => { + // The incoming calls may be for another room, which we will ignore. + if (newCall.roomId !== this.room.roomId) { + return; + } + if (newCall.state !== _call.CallState.Ringing) { + _logger.logger.warn(`GroupCall ${this.groupCallId} onIncomingCall() incoming call no longer in ringing state - ignoring`); + return; + } + if (!newCall.groupCallId || newCall.groupCallId !== this.groupCallId) { + _logger.logger.log(`GroupCall ${this.groupCallId} onIncomingCall() ignored because it doesn't match the current group call`); + newCall.reject(); + return; + } + const opponentUserId = newCall.getOpponentMember()?.userId; + if (opponentUserId === undefined) { + _logger.logger.warn(`GroupCall ${this.groupCallId} onIncomingCall() incoming call with no member - ignoring`); + return; + } + const deviceMap = this.calls.get(opponentUserId) ?? new Map(); + const prevCall = deviceMap.get(newCall.getOpponentDeviceId()); + if (prevCall?.callId === newCall.callId) return; + _logger.logger.log(`GroupCall ${this.groupCallId} onIncomingCall() incoming call (userId=${opponentUserId}, callId=${newCall.callId})`); + if (prevCall) this.disposeCall(prevCall, _call.CallErrorCode.Replaced); + this.initCall(newCall); + newCall.answerWithCallFeeds(this.getLocalFeeds().map(feed => feed.clone())); + deviceMap.set(newCall.getOpponentDeviceId(), newCall); + this.calls.set(opponentUserId, deviceMap); + this.emit(GroupCallEvent.CallsChanged, this.calls); + }); + _defineProperty(this, "onRetryCallLoop", () => { + let needsRetry = false; + for (const [{ + userId + }, participantMap] of this.participants) { + const callMap = this.calls.get(userId); + let retriesMap = this.retryCallCounts.get(userId); + for (const [deviceId, participant] of participantMap) { + const call = callMap?.get(deviceId); + const retries = retriesMap?.get(deviceId) ?? 0; + if (call?.getOpponentSessionId() !== participant.sessionId && this.wantsOutgoingCall(userId, deviceId) && retries < 3) { + if (retriesMap === undefined) { + retriesMap = new Map(); + this.retryCallCounts.set(userId, retriesMap); + } + retriesMap.set(deviceId, retries + 1); + needsRetry = true; + } + } + } + if (needsRetry) this.placeOutgoingCalls(); + }); + _defineProperty(this, "onCallFeedsChanged", call => { + const opponentMemberId = getCallUserId(call); + const opponentDeviceId = call.getOpponentDeviceId(); + if (!opponentMemberId) { + throw new Error("Cannot change call feeds without user id"); + } + const currentUserMediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId); + const remoteUsermediaFeed = call.remoteUsermediaFeed; + const remoteFeedChanged = remoteUsermediaFeed !== currentUserMediaFeed; + if (remoteFeedChanged) { + if (!currentUserMediaFeed && remoteUsermediaFeed) { + this.addUserMediaFeed(remoteUsermediaFeed); + } else if (currentUserMediaFeed && remoteUsermediaFeed) { + this.replaceUserMediaFeed(currentUserMediaFeed, remoteUsermediaFeed); + } else if (currentUserMediaFeed && !remoteUsermediaFeed) { + this.removeUserMediaFeed(currentUserMediaFeed); + } + } + const currentScreenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId); + const remoteScreensharingFeed = call.remoteScreensharingFeed; + const remoteScreenshareFeedChanged = remoteScreensharingFeed !== currentScreenshareFeed; + if (remoteScreenshareFeedChanged) { + if (!currentScreenshareFeed && remoteScreensharingFeed) { + this.addScreenshareFeed(remoteScreensharingFeed); + } else if (currentScreenshareFeed && remoteScreensharingFeed) { + this.replaceScreenshareFeed(currentScreenshareFeed, remoteScreensharingFeed); + } else if (currentScreenshareFeed && !remoteScreensharingFeed) { + this.removeScreenshareFeed(currentScreenshareFeed); + } + } + }); + _defineProperty(this, "onCallStateChanged", (call, state, _oldState) => { + const audioMuted = this.localCallFeed.isAudioMuted(); + if (call.localUsermediaStream && call.isMicrophoneMuted() !== audioMuted) { + call.setMicrophoneMuted(audioMuted); + } + const videoMuted = this.localCallFeed.isVideoMuted(); + if (call.localUsermediaStream && call.isLocalVideoMuted() !== videoMuted) { + call.setLocalVideoMuted(videoMuted); + } + const opponentUserId = call.getOpponentMember()?.userId; + if (state === _call.CallState.Connected && opponentUserId) { + const retriesMap = this.retryCallCounts.get(opponentUserId); + retriesMap?.delete(call.getOpponentDeviceId()); + if (retriesMap?.size === 0) this.retryCallCounts.delete(opponentUserId); + } + }); + _defineProperty(this, "onCallHangup", call => { + if (call.hangupReason === _call.CallErrorCode.Replaced) return; + const opponentUserId = call.getOpponentMember()?.userId ?? this.room.getMember(call.invitee).userId; + const deviceMap = this.calls.get(opponentUserId); + + // Sanity check that this call is in fact in the map + if (deviceMap?.get(call.getOpponentDeviceId()) === call) { + this.disposeCall(call, call.hangupReason); + deviceMap.delete(call.getOpponentDeviceId()); + if (deviceMap.size === 0) this.calls.delete(opponentUserId); + this.emit(GroupCallEvent.CallsChanged, this.calls); + } + }); + _defineProperty(this, "onCallReplaced", (prevCall, newCall) => { + const opponentUserId = prevCall.getOpponentMember().userId; + let deviceMap = this.calls.get(opponentUserId); + if (deviceMap === undefined) { + deviceMap = new Map(); + this.calls.set(opponentUserId, deviceMap); + } + this.disposeCall(prevCall, _call.CallErrorCode.Replaced); + this.initCall(newCall); + deviceMap.set(prevCall.getOpponentDeviceId(), newCall); + this.emit(GroupCallEvent.CallsChanged, this.calls); + }); + _defineProperty(this, "onActiveSpeakerLoop", () => { + let topAvg = undefined; + let nextActiveSpeaker = undefined; + for (const callFeed of this.userMediaFeeds) { + if (callFeed.isLocal() && this.userMediaFeeds.length > 1) continue; + const total = callFeed.speakingVolumeSamples.reduce((acc, volume) => acc + Math.max(volume, _callFeed.SPEAKING_THRESHOLD)); + const avg = total / callFeed.speakingVolumeSamples.length; + if (!topAvg || avg > topAvg) { + topAvg = avg; + nextActiveSpeaker = callFeed; + } + } + if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker && topAvg && topAvg > _callFeed.SPEAKING_THRESHOLD) { + this.activeSpeaker = nextActiveSpeaker; + this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker); + } + }); + _defineProperty(this, "onRoomState", () => this.updateParticipants()); + _defineProperty(this, "onParticipantsChanged", () => { + if (this.state === GroupCallState.Entered) this.placeOutgoingCalls(); + }); + _defineProperty(this, "onStateChanged", (newState, oldState) => { + if (newState === GroupCallState.Entered || oldState === GroupCallState.Entered || newState === GroupCallState.Ended) { + // We either entered, left, or ended the call + this.updateParticipants(); + this.updateMemberState().catch(e => _logger.logger.error(`GroupCall ${this.groupCallId} onStateChanged() failed to update member state devices"`, e)); + } + }); + _defineProperty(this, "onLocalFeedsChanged", () => { + if (this.state === GroupCallState.Entered) { + this.updateMemberState().catch(e => _logger.logger.error(`GroupCall ${this.groupCallId} onLocalFeedsChanged() failed to update member state feeds`, e)); + } + }); + this.reEmitter = new _ReEmitter.ReEmitter(this); + this.groupCallId = groupCallId ?? (0, _call.genCallID)(); + this.creationTs = room.currentState.getStateEvents(_event.EventType.GroupCallPrefix, this.groupCallId)?.getTs() ?? null; + this.updateParticipants(); + room.on(_roomState.RoomStateEvent.Update, this.onRoomState); + this.on(GroupCallEvent.ParticipantsChanged, this.onParticipantsChanged); + this.on(GroupCallEvent.GroupCallStateChanged, this.onStateChanged); + this.on(GroupCallEvent.LocalScreenshareStateChanged, this.onLocalFeedsChanged); + this.allowCallWithoutVideoAndAudio = !!isCallWithoutVideoAndAudio; + } + async create() { + this.creationTs = Date.now(); + this.client.groupCallEventHandler.groupCalls.set(this.room.roomId, this); + this.client.emit(_groupCallEventHandler.GroupCallEventHandlerEvent.Outgoing, this); + const groupCallState = { + "m.intent": this.intent, + "m.type": this.type, + "io.element.ptt": this.isPtt, + // TODO: Specify data-channels better + "dataChannelsEnabled": this.dataChannelsEnabled, + "dataChannelOptions": this.dataChannelsEnabled ? this.dataChannelOptions : undefined + }; + await this.client.sendStateEvent(this.room.roomId, _event.EventType.GroupCallPrefix, groupCallState, this.groupCallId); + return this; + } + /** + * The group call's state. + */ + get state() { + return this._state; + } + set state(value) { + const prevValue = this._state; + if (value !== prevValue) { + this._state = value; + this.emit(GroupCallEvent.GroupCallStateChanged, value, prevValue); + } + } + /** + * The current participants in the call, as a map from members to device IDs + * to participant info. + */ + get participants() { + return this._participants; + } + set participants(value) { + const prevValue = this._participants; + const participantStateEqual = (x, y) => x.sessionId === y.sessionId && x.screensharing === y.screensharing; + const deviceMapsEqual = (x, y) => (0, _utils.mapsEqual)(x, y, participantStateEqual); + + // Only update if the map actually changed + if (!(0, _utils.mapsEqual)(value, prevValue, deviceMapsEqual)) { + this._participants = value; + this.emit(GroupCallEvent.ParticipantsChanged, value); + } + } + /** + * The timestamp at which the call was created, or null if it has not yet + * been created. + */ + get creationTs() { + return this._creationTs; + } + set creationTs(value) { + this._creationTs = value; + } + /** + * Whether the local device has entered this call via another session, such + * as a widget. + */ + get enteredViaAnotherSession() { + return this._enteredViaAnotherSession; + } + set enteredViaAnotherSession(value) { + this._enteredViaAnotherSession = value; + this.updateParticipants(); + } + + /** + * Executes the given callback on all calls in this group call. + * @param f - The callback. + */ + forEachCall(f) { + for (const deviceMap of this.calls.values()) { + for (const call of deviceMap.values()) f(call); + } + } + getLocalFeeds() { + const feeds = []; + if (this.localCallFeed) feeds.push(this.localCallFeed); + if (this.localScreenshareFeed) feeds.push(this.localScreenshareFeed); + return feeds; + } + hasLocalParticipant() { + return this.participants.get(this.room.getMember(this.client.getUserId()))?.has(this.client.getDeviceId()) ?? false; + } + async initLocalCallFeed() { + if (this.state !== GroupCallState.LocalCallFeedUninitialized) { + throw new Error(`Cannot initialize local call feed in the "${this.state}" state.`); + } + this.state = GroupCallState.InitializingLocalCallFeed; + + // wraps the real method to serialise calls, because we don't want to try starting + // multiple call feeds at once + if (this.initCallFeedPromise) return this.initCallFeedPromise; + try { + this.initCallFeedPromise = this.initLocalCallFeedInternal(); + await this.initCallFeedPromise; + } finally { + this.initCallFeedPromise = undefined; + } + } + async initLocalCallFeedInternal() { + _logger.logger.log(`GroupCall ${this.groupCallId} initLocalCallFeedInternal() running`); + let stream; + try { + stream = await this.client.getMediaHandler().getUserMediaStream(true, this.type === GroupCallType.Video); + } catch (error) { + // If is allowed to join a call without a media stream, then we + // don't throw an error here. But we need an empty Local Feed to establish + // a connection later. + if (this.allowCallWithoutVideoAndAudio) { + stream = new MediaStream(); + } else { + this.state = GroupCallState.LocalCallFeedUninitialized; + throw error; + } + } + + // The call could've been disposed while we were waiting, and could + // also have been started back up again (hello, React 18) so if we're + // still in this 'initializing' state, carry on, otherwise bail. + if (this._state !== GroupCallState.InitializingLocalCallFeed) { + this.client.getMediaHandler().stopUserMediaStream(stream); + throw new Error("Group call disposed while gathering media stream"); + } + const callFeed = new _callFeed.CallFeed({ + client: this.client, + roomId: this.room.roomId, + userId: this.client.getUserId(), + deviceId: this.client.getDeviceId(), + stream, + purpose: _callEventTypes.SDPStreamMetadataPurpose.Usermedia, + audioMuted: this.initWithAudioMuted || stream.getAudioTracks().length === 0 || this.isPtt, + videoMuted: this.initWithVideoMuted || stream.getVideoTracks().length === 0 + }); + (0, _call.setTracksEnabled)(stream.getAudioTracks(), !callFeed.isAudioMuted()); + (0, _call.setTracksEnabled)(stream.getVideoTracks(), !callFeed.isVideoMuted()); + this.localCallFeed = callFeed; + this.addUserMediaFeed(callFeed); + this.state = GroupCallState.LocalCallFeedInitialized; + } + async updateLocalUsermediaStream(stream) { + if (this.localCallFeed) { + const oldStream = this.localCallFeed.stream; + this.localCallFeed.setNewStream(stream); + const micShouldBeMuted = this.localCallFeed.isAudioMuted(); + const vidShouldBeMuted = this.localCallFeed.isVideoMuted(); + _logger.logger.log(`GroupCall ${this.groupCallId} updateLocalUsermediaStream() (oldStreamId=${oldStream.id}, newStreamId=${stream.id}, micShouldBeMuted=${micShouldBeMuted}, vidShouldBeMuted=${vidShouldBeMuted})`); + (0, _call.setTracksEnabled)(stream.getAudioTracks(), !micShouldBeMuted); + (0, _call.setTracksEnabled)(stream.getVideoTracks(), !vidShouldBeMuted); + this.client.getMediaHandler().stopUserMediaStream(oldStream); + } + } + async enter() { + if (this.state === GroupCallState.LocalCallFeedUninitialized) { + await this.initLocalCallFeed(); + } else if (this.state !== GroupCallState.LocalCallFeedInitialized) { + throw new Error(`Cannot enter call in the "${this.state}" state`); + } + _logger.logger.log(`GroupCall ${this.groupCallId} enter() running`); + this.state = GroupCallState.Entered; + this.client.on(_callEventHandler.CallEventHandlerEvent.Incoming, this.onIncomingCall); + for (const call of this.client.callEventHandler.calls.values()) { + this.onIncomingCall(call); + } + this.retryCallLoopInterval = setInterval(this.onRetryCallLoop, this.retryCallInterval); + this.activeSpeaker = undefined; + this.onActiveSpeakerLoop(); + this.activeSpeakerLoopInterval = setInterval(this.onActiveSpeakerLoop, this.activeSpeakerInterval); + } + dispose() { + if (this.localCallFeed) { + this.removeUserMediaFeed(this.localCallFeed); + this.localCallFeed = undefined; + } + if (this.localScreenshareFeed) { + this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream); + this.removeScreenshareFeed(this.localScreenshareFeed); + this.localScreenshareFeed = undefined; + this.localDesktopCapturerSourceId = undefined; + } + this.client.getMediaHandler().stopAllStreams(); + if (this.transmitTimer !== null) { + clearTimeout(this.transmitTimer); + this.transmitTimer = null; + } + if (this.retryCallLoopInterval !== undefined) { + clearInterval(this.retryCallLoopInterval); + this.retryCallLoopInterval = undefined; + } + if (this.state !== GroupCallState.Entered) { + return; + } + this.forEachCall(call => this.disposeCall(call, _call.CallErrorCode.UserHangup)); + this.calls.clear(); + this.activeSpeaker = undefined; + clearInterval(this.activeSpeakerLoopInterval); + this.retryCallCounts.clear(); + clearInterval(this.retryCallLoopInterval); + this.client.removeListener(_callEventHandler.CallEventHandlerEvent.Incoming, this.onIncomingCall); + } + leave() { + this.dispose(); + this.state = GroupCallState.LocalCallFeedUninitialized; + } + async terminate(emitStateEvent = true) { + this.dispose(); + this.room.off(_roomState.RoomStateEvent.Update, this.onRoomState); + this.client.groupCallEventHandler.groupCalls.delete(this.room.roomId); + this.client.emit(_groupCallEventHandler.GroupCallEventHandlerEvent.Ended, this); + this.state = GroupCallState.Ended; + if (emitStateEvent) { + const existingStateEvent = this.room.currentState.getStateEvents(_event.EventType.GroupCallPrefix, this.groupCallId); + await this.client.sendStateEvent(this.room.roomId, _event.EventType.GroupCallPrefix, _objectSpread(_objectSpread({}, existingStateEvent.getContent()), {}, { + "m.terminated": GroupCallTerminationReason.CallEnded + }), this.groupCallId); + } + } + + /* + * Local Usermedia + */ + + isLocalVideoMuted() { + if (this.localCallFeed) { + return this.localCallFeed.isVideoMuted(); + } + return true; + } + isMicrophoneMuted() { + if (this.localCallFeed) { + return this.localCallFeed.isAudioMuted(); + } + return true; + } + + /** + * Sets the mute state of the local participants's microphone. + * @param muted - Whether to mute the microphone + * @returns Whether muting/unmuting was successful + */ + async setMicrophoneMuted(muted) { + // hasAudioDevice can block indefinitely if the window has lost focus, + // and it doesn't make much sense to keep a device from being muted, so + // we always allow muted = true changes to go through + if (!muted && !(await this.client.getMediaHandler().hasAudioDevice())) { + return false; + } + const sendUpdatesBefore = !muted && this.isPtt; + + // set a timer for the maximum transmit time on PTT calls + if (this.isPtt) { + // Set or clear the max transmit timer + if (!muted && this.isMicrophoneMuted()) { + this.transmitTimer = setTimeout(() => { + this.setMicrophoneMuted(true); + }, this.pttMaxTransmitTime); + } else if (muted && !this.isMicrophoneMuted()) { + if (this.transmitTimer !== null) clearTimeout(this.transmitTimer); + this.transmitTimer = null; + } + } + this.forEachCall(call => call.localUsermediaFeed?.setAudioVideoMuted(muted, null)); + const sendUpdates = async () => { + const updates = []; + this.forEachCall(call => updates.push(call.sendMetadataUpdate())); + await Promise.all(updates).catch(e => _logger.logger.info(`GroupCall ${this.groupCallId} setMicrophoneMuted() failed to send some metadata updates`, e)); + }; + if (sendUpdatesBefore) await sendUpdates(); + if (this.localCallFeed) { + _logger.logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() (streamId=${this.localCallFeed.stream.id}, muted=${muted})`); + + // We needed this here to avoid an error in case user join a call without a device. + // I can not use .then .catch functions because linter :-( + try { + if (!muted) { + const stream = await this.client.getMediaHandler().getUserMediaStream(true, !this.localCallFeed.isVideoMuted()); + if (stream === null) { + // if case permission denied to get a stream stop this here + /* istanbul ignore next */ + _logger.logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() no device to receive local stream, muted=${muted}`); + return false; + } + } + } catch (e) { + /* istanbul ignore next */ + _logger.logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`); + return false; + } + this.localCallFeed.setAudioVideoMuted(muted, null); + // I don't believe its actually necessary to enable these tracks: they + // are the one on the GroupCall's own CallFeed and are cloned before being + // given to any of the actual calls, so these tracks don't actually go + // anywhere. Let's do it anyway to avoid confusion. + (0, _call.setTracksEnabled)(this.localCallFeed.stream.getAudioTracks(), !muted); + } else { + _logger.logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() no stream muted (muted=${muted})`); + this.initWithAudioMuted = muted; + } + this.forEachCall(call => (0, _call.setTracksEnabled)(call.localUsermediaFeed.stream.getAudioTracks(), !muted)); + this.emit(GroupCallEvent.LocalMuteStateChanged, muted, this.isLocalVideoMuted()); + if (!sendUpdatesBefore) await sendUpdates(); + return true; + } + + /** + * Sets the mute state of the local participants's video. + * @param muted - Whether to mute the video + * @returns Whether muting/unmuting was successful + */ + async setLocalVideoMuted(muted) { + // hasAudioDevice can block indefinitely if the window has lost focus, + // and it doesn't make much sense to keep a device from being muted, so + // we always allow muted = true changes to go through + if (!muted && !(await this.client.getMediaHandler().hasVideoDevice())) { + return false; + } + if (this.localCallFeed) { + /* istanbul ignore next */ + _logger.logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() (stream=${this.localCallFeed.stream.id}, muted=${muted})`); + try { + const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted); + await this.updateLocalUsermediaStream(stream); + this.localCallFeed.setAudioVideoMuted(null, muted); + (0, _call.setTracksEnabled)(this.localCallFeed.stream.getVideoTracks(), !muted); + } catch (_) { + // No permission to video device + /* istanbul ignore next */ + _logger.logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no device or permission to receive local stream, muted=${muted}`); + return false; + } + } else { + _logger.logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no stream muted (muted=${muted})`); + this.initWithVideoMuted = muted; + } + const updates = []; + this.forEachCall(call => updates.push(call.setLocalVideoMuted(muted))); + await Promise.all(updates); + this.emit(GroupCallEvent.LocalMuteStateChanged, this.isMicrophoneMuted(), muted); + return true; + } + async setScreensharingEnabled(enabled, opts = {}) { + if (enabled === this.isScreensharing()) { + return enabled; + } + if (enabled) { + try { + _logger.logger.log(`GroupCall ${this.groupCallId} setScreensharingEnabled() is asking for screensharing permissions`); + const stream = await this.client.getMediaHandler().getScreensharingStream(opts); + for (const track of stream.getTracks()) { + const onTrackEnded = () => { + this.setScreensharingEnabled(false); + track.removeEventListener("ended", onTrackEnded); + }; + track.addEventListener("ended", onTrackEnded); + } + _logger.logger.log(`GroupCall ${this.groupCallId} setScreensharingEnabled() granted screensharing permissions. Setting screensharing enabled on all calls`); + this.localDesktopCapturerSourceId = opts.desktopCapturerSourceId; + this.localScreenshareFeed = new _callFeed.CallFeed({ + client: this.client, + roomId: this.room.roomId, + userId: this.client.getUserId(), + deviceId: this.client.getDeviceId(), + stream, + purpose: _callEventTypes.SDPStreamMetadataPurpose.Screenshare, + audioMuted: false, + videoMuted: false + }); + this.addScreenshareFeed(this.localScreenshareFeed); + this.emit(GroupCallEvent.LocalScreenshareStateChanged, true, this.localScreenshareFeed, this.localDesktopCapturerSourceId); + + // TODO: handle errors + this.forEachCall(call => call.pushLocalFeed(this.localScreenshareFeed.clone())); + return true; + } catch (error) { + if (opts.throwOnFail) throw error; + _logger.logger.error(`GroupCall ${this.groupCallId} setScreensharingEnabled() enabling screensharing error`, error); + this.emit(GroupCallEvent.Error, new GroupCallError(GroupCallErrorCode.NoUserMedia, "Failed to get screen-sharing stream: ", error)); + return false; + } + } else { + this.forEachCall(call => { + if (call.localScreensharingFeed) call.removeLocalFeed(call.localScreensharingFeed); + }); + this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream); + this.removeScreenshareFeed(this.localScreenshareFeed); + this.localScreenshareFeed = undefined; + this.localDesktopCapturerSourceId = undefined; + this.emit(GroupCallEvent.LocalScreenshareStateChanged, false, undefined, undefined); + return false; + } + } + isScreensharing() { + return !!this.localScreenshareFeed; + } + + /* + * Call Setup + * + * There are two different paths for calls to be created: + * 1. Incoming calls triggered by the Call.incoming event. + * 2. Outgoing calls to the initial members of a room or new members + * as they are observed by the RoomState.members event. + */ + + /** + * Determines whether a given participant expects us to call them (versus + * them calling us). + * @param userId - The participant's user ID. + * @param deviceId - The participant's device ID. + * @returns Whether we need to place an outgoing call to the participant. + */ + wantsOutgoingCall(userId, deviceId) { + const localUserId = this.client.getUserId(); + const localDeviceId = this.client.getDeviceId(); + return ( + // If a user's ID is less than our own, they'll call us + userId >= localUserId && ( + // If this is another one of our devices, compare device IDs to tell whether it'll call us + userId !== localUserId || deviceId > localDeviceId) + ); + } + + /** + * Places calls to all participants that we're responsible for calling. + */ + placeOutgoingCalls() { + let callsChanged = false; + for (const [{ + userId + }, participantMap] of this.participants) { + const callMap = this.calls.get(userId) ?? new Map(); + for (const [deviceId, participant] of participantMap) { + const prevCall = callMap.get(deviceId); + if (prevCall?.getOpponentSessionId() !== participant.sessionId && this.wantsOutgoingCall(userId, deviceId)) { + callsChanged = true; + if (prevCall !== undefined) { + _logger.logger.debug(`GroupCall ${this.groupCallId} placeOutgoingCalls() replacing call (userId=${userId}, deviceId=${deviceId}, callId=${prevCall.callId})`); + this.disposeCall(prevCall, _call.CallErrorCode.NewSession); + } + const newCall = (0, _call.createNewMatrixCall)(this.client, this.room.roomId, { + invitee: userId, + opponentDeviceId: deviceId, + opponentSessionId: participant.sessionId, + groupCallId: this.groupCallId + }); + if (newCall === null) { + _logger.logger.error(`GroupCall ${this.groupCallId} placeOutgoingCalls() failed to create call (userId=${userId}, device=${deviceId})`); + callMap.delete(deviceId); + } else { + this.initCall(newCall); + callMap.set(deviceId, newCall); + _logger.logger.debug(`GroupCall ${this.groupCallId} placeOutgoingCalls() placing call (userId=${userId}, deviceId=${deviceId}, sessionId=${participant.sessionId})`); + newCall.placeCallWithCallFeeds(this.getLocalFeeds().map(feed => feed.clone()), participant.screensharing).then(() => { + if (this.dataChannelsEnabled) { + newCall.createDataChannel("datachannel", this.dataChannelOptions); + } + }).catch(e => { + _logger.logger.warn(`GroupCall ${this.groupCallId} placeOutgoingCalls() failed to place call (userId=${userId})`, e); + if (e instanceof _call.CallError && e.code === GroupCallErrorCode.UnknownDevice) { + this.emit(GroupCallEvent.Error, e); + } else { + this.emit(GroupCallEvent.Error, new GroupCallError(GroupCallErrorCode.PlaceCallFailed, `Failed to place call to ${userId}`)); + } + this.disposeCall(newCall, _call.CallErrorCode.SignallingFailed); + if (callMap.get(deviceId) === newCall) callMap.delete(deviceId); + }); + } + } + } + if (callMap.size > 0) { + this.calls.set(userId, callMap); + } else { + this.calls.delete(userId); + } + } + if (callsChanged) this.emit(GroupCallEvent.CallsChanged, this.calls); + } + + /* + * Room Member State + */ + + getMemberStateEvents(userId) { + return userId === undefined ? this.room.currentState.getStateEvents(_event.EventType.GroupCallMemberPrefix) : this.room.currentState.getStateEvents(_event.EventType.GroupCallMemberPrefix, userId); + } + initCall(call) { + const opponentMemberId = getCallUserId(call); + if (!opponentMemberId) { + throw new Error("Cannot init call without user id"); + } + const onCallFeedsChanged = () => this.onCallFeedsChanged(call); + const onCallStateChanged = (state, oldState) => this.onCallStateChanged(call, state, oldState); + const onCallHangup = this.onCallHangup; + const onCallReplaced = newCall => this.onCallReplaced(call, newCall); + let deviceMap = this.callHandlers.get(opponentMemberId); + if (deviceMap === undefined) { + deviceMap = new Map(); + this.callHandlers.set(opponentMemberId, deviceMap); + } + deviceMap.set(call.getOpponentDeviceId(), { + onCallFeedsChanged, + onCallStateChanged, + onCallHangup, + onCallReplaced + }); + call.on(_call.CallEvent.FeedsChanged, onCallFeedsChanged); + call.on(_call.CallEvent.State, onCallStateChanged); + call.on(_call.CallEvent.Hangup, onCallHangup); + call.on(_call.CallEvent.Replaced, onCallReplaced); + call.isPtt = this.isPtt; + this.reEmitter.reEmit(call, Object.values(_call.CallEvent)); + onCallFeedsChanged(); + } + disposeCall(call, hangupReason) { + const opponentMemberId = getCallUserId(call); + const opponentDeviceId = call.getOpponentDeviceId(); + if (!opponentMemberId) { + throw new Error("Cannot dispose call without user id"); + } + const deviceMap = this.callHandlers.get(opponentMemberId); + const { + onCallFeedsChanged, + onCallStateChanged, + onCallHangup, + onCallReplaced + } = deviceMap.get(opponentDeviceId); + call.removeListener(_call.CallEvent.FeedsChanged, onCallFeedsChanged); + call.removeListener(_call.CallEvent.State, onCallStateChanged); + call.removeListener(_call.CallEvent.Hangup, onCallHangup); + call.removeListener(_call.CallEvent.Replaced, onCallReplaced); + deviceMap.delete(opponentMemberId); + if (deviceMap.size === 0) this.callHandlers.delete(opponentMemberId); + if (call.hangupReason === _call.CallErrorCode.Replaced) { + return; + } + if (call.state !== _call.CallState.Ended) { + call.hangup(hangupReason, false); + } + const usermediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId); + if (usermediaFeed) { + this.removeUserMediaFeed(usermediaFeed); + } + const screenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId); + if (screenshareFeed) { + this.removeScreenshareFeed(screenshareFeed); + } + } + /* + * UserMedia CallFeed Event Handlers + */ + + getUserMediaFeed(userId, deviceId) { + return this.userMediaFeeds.find(f => f.userId === userId && f.deviceId === deviceId); + } + addUserMediaFeed(callFeed) { + this.userMediaFeeds.push(callFeed); + callFeed.measureVolumeActivity(true); + this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds); + } + replaceUserMediaFeed(existingFeed, replacementFeed) { + const feedIndex = this.userMediaFeeds.findIndex(f => f.userId === existingFeed.userId && f.deviceId === existingFeed.deviceId); + if (feedIndex === -1) { + throw new Error("Couldn't find user media feed to replace"); + } + this.userMediaFeeds.splice(feedIndex, 1, replacementFeed); + existingFeed.dispose(); + replacementFeed.measureVolumeActivity(true); + this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds); + } + removeUserMediaFeed(callFeed) { + const feedIndex = this.userMediaFeeds.findIndex(f => f.userId === callFeed.userId && f.deviceId === callFeed.deviceId); + if (feedIndex === -1) { + throw new Error("Couldn't find user media feed to remove"); + } + this.userMediaFeeds.splice(feedIndex, 1); + callFeed.dispose(); + this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds); + if (this.activeSpeaker === callFeed) { + this.activeSpeaker = this.userMediaFeeds[0]; + this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker); + } + } + /* + * Screenshare Call Feed Event Handlers + */ + + getScreenshareFeed(userId, deviceId) { + return this.screenshareFeeds.find(f => f.userId === userId && f.deviceId === deviceId); + } + addScreenshareFeed(callFeed) { + this.screenshareFeeds.push(callFeed); + this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds); + } + replaceScreenshareFeed(existingFeed, replacementFeed) { + const feedIndex = this.screenshareFeeds.findIndex(f => f.userId === existingFeed.userId && f.deviceId === existingFeed.deviceId); + if (feedIndex === -1) { + throw new Error("Couldn't find screenshare feed to replace"); + } + this.screenshareFeeds.splice(feedIndex, 1, replacementFeed); + existingFeed.dispose(); + this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds); + } + removeScreenshareFeed(callFeed) { + const feedIndex = this.screenshareFeeds.findIndex(f => f.userId === callFeed.userId && f.deviceId === callFeed.deviceId); + if (feedIndex === -1) { + throw new Error("Couldn't find screenshare feed to remove"); + } + this.screenshareFeeds.splice(feedIndex, 1); + callFeed.dispose(); + this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds); + } + + /** + * Recalculates and updates the participant map to match the room state. + */ + updateParticipants() { + const localMember = this.room.getMember(this.client.getUserId()); + if (!localMember) { + // The client hasn't fetched enough of the room state to get our own member + // event. This probably shouldn't happen, but sanity check & exit for now. + _logger.logger.warn(`GroupCall ${this.groupCallId} updateParticipants() tried to update participants before local room member is available`); + return; + } + if (this.participantsExpirationTimer !== null) { + clearTimeout(this.participantsExpirationTimer); + this.participantsExpirationTimer = null; + } + if (this.state === GroupCallState.Ended) { + this.participants = new Map(); + return; + } + const participants = new Map(); + const now = Date.now(); + const entered = this.state === GroupCallState.Entered || this.enteredViaAnotherSession; + let nextExpiration = Infinity; + for (const e of this.getMemberStateEvents()) { + const member = this.room.getMember(e.getStateKey()); + const content = e.getContent(); + const calls = Array.isArray(content["m.calls"]) ? content["m.calls"] : []; + const call = calls.find(call => call["m.call_id"] === this.groupCallId); + const devices = Array.isArray(call?.["m.devices"]) ? call["m.devices"] : []; + + // Filter out invalid and expired devices + let validDevices = devices.filter(d => typeof d.device_id === "string" && typeof d.session_id === "string" && typeof d.expires_ts === "number" && d.expires_ts > now && Array.isArray(d.feeds)); + + // Apply local echo for the unentered case + if (!entered && member?.userId === this.client.getUserId()) { + validDevices = validDevices.filter(d => d.device_id !== this.client.getDeviceId()); + } + + // Must have a connected device and be joined to the room + if (validDevices.length > 0 && member?.membership === "join") { + const deviceMap = new Map(); + participants.set(member, deviceMap); + for (const d of validDevices) { + deviceMap.set(d.device_id, { + sessionId: d.session_id, + screensharing: d.feeds.some(f => f.purpose === _callEventTypes.SDPStreamMetadataPurpose.Screenshare) + }); + if (d.expires_ts < nextExpiration) nextExpiration = d.expires_ts; + } + } + } + + // Apply local echo for the entered case + if (entered) { + let deviceMap = participants.get(localMember); + if (deviceMap === undefined) { + deviceMap = new Map(); + participants.set(localMember, deviceMap); + } + if (!deviceMap.has(this.client.getDeviceId())) { + deviceMap.set(this.client.getDeviceId(), { + sessionId: this.client.getSessionId(), + screensharing: this.getLocalFeeds().some(f => f.purpose === _callEventTypes.SDPStreamMetadataPurpose.Screenshare) + }); + } + } + this.participants = participants; + if (nextExpiration < Infinity) { + this.participantsExpirationTimer = setTimeout(() => this.updateParticipants(), nextExpiration - now); + } + } + + /** + * Updates the local user's member state with the devices returned by the given function. + * @param fn - A function from the current devices to the new devices. If it + * returns null, the update will be skipped. + * @param keepAlive - Whether the request should outlive the window. + */ + async updateDevices(fn, keepAlive = false) { + const now = Date.now(); + const localUserId = this.client.getUserId(); + const event = this.getMemberStateEvents(localUserId); + const content = event?.getContent() ?? {}; + const calls = Array.isArray(content["m.calls"]) ? content["m.calls"] : []; + let call = null; + const otherCalls = []; + for (const c of calls) { + if (c["m.call_id"] === this.groupCallId) { + call = c; + } else { + otherCalls.push(c); + } + } + if (call === null) call = {}; + const devices = Array.isArray(call["m.devices"]) ? call["m.devices"] : []; + + // Filter out invalid and expired devices + const validDevices = devices.filter(d => typeof d.device_id === "string" && typeof d.session_id === "string" && typeof d.expires_ts === "number" && d.expires_ts > now && Array.isArray(d.feeds)); + const newDevices = fn(validDevices); + if (newDevices === null) return; + const newCalls = [...otherCalls]; + if (newDevices.length > 0) { + newCalls.push(_objectSpread(_objectSpread({}, call), {}, { + "m.call_id": this.groupCallId, + "m.devices": newDevices + })); + } + const newContent = { + "m.calls": newCalls + }; + await this.client.sendStateEvent(this.room.roomId, _event.EventType.GroupCallMemberPrefix, newContent, localUserId, { + keepAlive + }); + } + async addDeviceToMemberState() { + await this.updateDevices(devices => [...devices.filter(d => d.device_id !== this.client.getDeviceId()), { + device_id: this.client.getDeviceId(), + session_id: this.client.getSessionId(), + expires_ts: Date.now() + DEVICE_TIMEOUT, + feeds: this.getLocalFeeds().map(feed => ({ + purpose: feed.purpose + })) + // TODO: Add data channels + }]); + } + + async updateMemberState() { + // Clear the old update interval before proceeding + if (this.resendMemberStateTimer !== null) { + clearInterval(this.resendMemberStateTimer); + this.resendMemberStateTimer = null; + } + if (this.state === GroupCallState.Entered) { + // Add the local device + await this.addDeviceToMemberState(); + + // Resend the state event every so often so it doesn't become stale + this.resendMemberStateTimer = setInterval(async () => { + _logger.logger.log(`GroupCall ${this.groupCallId} updateMemberState() resending call member state"`); + try { + await this.addDeviceToMemberState(); + } catch (e) { + _logger.logger.error(`GroupCall ${this.groupCallId} updateMemberState() failed to resend call member state`, e); + } + }, DEVICE_TIMEOUT * 3 / 4); + } else { + // Remove the local device + await this.updateDevices(devices => devices.filter(d => d.device_id !== this.client.getDeviceId()), true); + } + } + + /** + * Cleans up our member state by filtering out logged out devices, inactive + * devices, and our own device (if we know we haven't entered). + */ + async cleanMemberState() { + const { + devices: myDevices + } = await this.client.getDevices(); + const deviceMap = new Map(myDevices.map(d => [d.device_id, d])); + + // updateDevices takes care of filtering out inactive devices for us + await this.updateDevices(devices => { + const newDevices = devices.filter(d => { + const device = deviceMap.get(d.device_id); + return device?.last_seen_ts !== undefined && !(d.device_id === this.client.getDeviceId() && this.state !== GroupCallState.Entered && !this.enteredViaAnotherSession); + }); + + // Skip the update if the devices are unchanged + return newDevices.length === devices.length ? null : newDevices; + }); + } +} +exports.GroupCall = GroupCall; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/mediaHandler.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/mediaHandler.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/mediaHandler.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-sdk/webrtc/mediaHandler.js 2023-04-11 06:11:52.000000000 +0000 @@ -3,267 +3,338 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.MediaHandler = void 0; - +exports.MediaHandlerEvent = exports.MediaHandler = void 0; +var _typedEventEmitter = require("../models/typed-event-emitter"); +var _groupCall = require("../webrtc/groupCall"); var _logger = require("../logger"); +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } +function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +let MediaHandlerEvent; +exports.MediaHandlerEvent = MediaHandlerEvent; +(function (MediaHandlerEvent) { + MediaHandlerEvent["LocalStreamsChanged"] = "local_streams_changed"; +})(MediaHandlerEvent || (exports.MediaHandlerEvent = MediaHandlerEvent = {})); +class MediaHandler extends _typedEventEmitter.TypedEventEmitter { + // Promise chain to serialise calls to getMediaStream -var _call = require("./call"); - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -class MediaHandler { constructor(client) { + super(); this.client = client; - _defineProperty(this, "audioInput", void 0); - + _defineProperty(this, "audioSettings", void 0); _defineProperty(this, "videoInput", void 0); - _defineProperty(this, "localUserMediaStream", void 0); - _defineProperty(this, "userMediaStreams", []); - _defineProperty(this, "screensharingStreams", []); + _defineProperty(this, "getMediaStreamPromise", void 0); + } + restoreMediaSettings(audioInput, videoInput) { + this.audioInput = audioInput; + this.videoInput = videoInput; } + /** * Set an audio input device to use for MatrixCalls - * @param {string} deviceId the identifier for the device + * @param deviceId - the identifier for the device * undefined treated as unset */ - - async setAudioInput(deviceId) { - _logger.logger.info("LOG setting audio input to", deviceId); - + _logger.logger.info(`MediaHandler setAudioInput() running (deviceId=${deviceId})`); if (this.audioInput === deviceId) return; this.audioInput = deviceId; await this.updateLocalUsermediaStreams(); } + + /** + * Set audio settings for MatrixCalls + * @param opts - audio options to set + */ + async setAudioSettings(opts) { + _logger.logger.info(`MediaHandler setAudioSettings() running (opts=${JSON.stringify(opts)})`); + this.audioSettings = Object.assign({}, opts); + await this.updateLocalUsermediaStreams(); + } + /** * Set a video input device to use for MatrixCalls - * @param {string} deviceId the identifier for the device + * @param deviceId - the identifier for the device * undefined treated as unset */ - - async setVideoInput(deviceId) { - _logger.logger.info("LOG setting video input to", deviceId); - + _logger.logger.info(`MediaHandler setVideoInput() running (deviceId=${deviceId})`); if (this.videoInput === deviceId) return; this.videoInput = deviceId; await this.updateLocalUsermediaStreams(); } + /** - * Requests new usermedia streams and replace the old ones + * Set media input devices to use for MatrixCalls + * @param audioInput - the identifier for the audio device + * @param videoInput - the identifier for the video device + * undefined treated as unset */ + async setMediaInputs(audioInput, videoInput) { + _logger.logger.log(`MediaHandler setMediaInputs() running (audioInput: ${audioInput} videoInput: ${videoInput})`); + this.audioInput = audioInput; + this.videoInput = videoInput; + await this.updateLocalUsermediaStreams(); + } - + /* + * Requests new usermedia streams and replace the old ones + */ async updateLocalUsermediaStreams() { if (this.userMediaStreams.length === 0) return; const callMediaStreamParams = new Map(); - for (const call of this.client.callEventHandler.calls.values()) { callMediaStreamParams.set(call.callId, { audio: call.hasLocalUserMediaAudioTrack, video: call.hasLocalUserMediaVideoTrack }); } - + for (const stream of this.userMediaStreams) { + _logger.logger.log(`MediaHandler updateLocalUsermediaStreams() stopping all tracks (streamId=${stream.id})`); + for (const track of stream.getTracks()) { + track.stop(); + } + } + this.userMediaStreams = []; + this.localUserMediaStream = undefined; for (const call of this.client.callEventHandler.calls.values()) { - if (call.state === _call.CallState.Ended || !callMediaStreamParams.has(call.callId)) continue; + if (call.callHasEnded() || !callMediaStreamParams.has(call.callId)) { + continue; + } const { audio, video - } = callMediaStreamParams.get(call.callId); // This stream won't be reusable as we will replace the tracks of the old stream - - const stream = await this.getUserMediaStream(audio, video, false); + } = callMediaStreamParams.get(call.callId); + _logger.logger.log(`MediaHandler updateLocalUsermediaStreams() calling getUserMediaStream() (callId=${call.callId})`); + const stream = await this.getUserMediaStream(audio, video); + if (call.callHasEnded()) { + continue; + } await call.updateLocalUsermediaStream(stream); } + for (const groupCall of this.client.groupCallEventHandler.groupCalls.values()) { + if (!groupCall.localCallFeed) { + continue; + } + _logger.logger.log(`MediaHandler updateLocalUsermediaStreams() calling getUserMediaStream() (groupCallId=${groupCall.groupCallId})`); + const stream = await this.getUserMediaStream(true, groupCall.type === _groupCall.GroupCallType.Video); + if (groupCall.state === _groupCall.GroupCallState.Ended) { + continue; + } + await groupCall.updateLocalUsermediaStream(stream); + } + this.emit(MediaHandlerEvent.LocalStreamsChanged); } - async hasAudioDevice() { - const devices = await navigator.mediaDevices.enumerateDevices(); - return devices.filter(device => device.kind === "audioinput").length > 0; + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + return devices.filter(device => device.kind === "audioinput").length > 0; + } catch (err) { + _logger.logger.log(`MediaHandler hasAudioDevice() calling navigator.mediaDevices.enumerateDevices with error`, err); + return false; + } } - async hasVideoDevice() { - const devices = await navigator.mediaDevices.enumerateDevices(); - return devices.filter(device => device.kind === "videoinput").length > 0; + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + return devices.filter(device => device.kind === "videoinput").length > 0; + } catch (err) { + _logger.logger.log(`MediaHandler hasVideoDevice() calling navigator.mediaDevices.enumerateDevices with error`, err); + return false; + } } + /** - * @param audio should have an audio track - * @param video should have a video track - * @param reusable is allowed to be reused by the MediaHandler - * @returns {MediaStream} based on passed parameters + * @param audio - should have an audio track + * @param video - should have a video track + * @param reusable - is allowed to be reused by the MediaHandler + * @returns based on passed parameters */ - - async getUserMediaStream(audio, video, reusable = true) { + // Serialise calls, othertwise we can't sensibly re-use the stream + if (this.getMediaStreamPromise) { + this.getMediaStreamPromise = this.getMediaStreamPromise.then(() => { + return this.getUserMediaStreamInternal(audio, video, reusable); + }); + } else { + this.getMediaStreamPromise = this.getUserMediaStreamInternal(audio, video, reusable); + } + return this.getMediaStreamPromise; + } + async getUserMediaStreamInternal(audio, video, reusable) { const shouldRequestAudio = audio && (await this.hasAudioDevice()); const shouldRequestVideo = video && (await this.hasVideoDevice()); let stream; + let canReuseStream = true; + if (this.localUserMediaStream) { + // This figures out if we can reuse the current localUsermediaStream + // based on whether or not the "mute state" (presence of tracks of a + // given kind) matches what is being requested + if (shouldRequestAudio !== this.localUserMediaStream.getAudioTracks().length > 0) { + canReuseStream = false; + } + if (shouldRequestVideo !== this.localUserMediaStream.getVideoTracks().length > 0) { + canReuseStream = false; + } - if (!this.localUserMediaStream || this.localUserMediaStream.getAudioTracks().length === 0 && shouldRequestAudio || this.localUserMediaStream.getVideoTracks().length === 0 && shouldRequestVideo || this.localUserMediaStream.getAudioTracks()[0]?.getSettings()?.deviceId !== this.audioInput || this.localUserMediaStream.getVideoTracks()[0]?.getSettings()?.deviceId !== this.videoInput) { + // This code checks that the device ID is the same as the localUserMediaStream stream, but we update + // the localUserMediaStream whenever the device ID changes (apart from when restoring) so it's not + // clear why this would ever be different, unless there's a race. + if (shouldRequestAudio && this.localUserMediaStream.getAudioTracks()[0]?.getSettings()?.deviceId !== this.audioInput) { + canReuseStream = false; + } + if (shouldRequestVideo && this.localUserMediaStream.getVideoTracks()[0]?.getSettings()?.deviceId !== this.videoInput) { + canReuseStream = false; + } + } else { + canReuseStream = false; + } + if (!canReuseStream) { const constraints = this.getUserMediaContraints(shouldRequestAudio, shouldRequestVideo); - - _logger.logger.log("Getting user media with constraints", constraints); - stream = await navigator.mediaDevices.getUserMedia(constraints); - + _logger.logger.log(`MediaHandler getUserMediaStreamInternal() calling getUserMediaStream (streamId=${stream.id}, shouldRequestAudio=${shouldRequestAudio}, shouldRequestVideo=${shouldRequestVideo}, constraints=${JSON.stringify(constraints)})`); for (const track of stream.getTracks()) { const settings = track.getSettings(); - if (track.kind === "audio") { this.audioInput = settings.deviceId; } else if (track.kind === "video") { this.videoInput = settings.deviceId; } } - if (reusable) { this.localUserMediaStream = stream; } } else { stream = this.localUserMediaStream.clone(); - + _logger.logger.log(`MediaHandler getUserMediaStreamInternal() cloning (oldStreamId=${this.localUserMediaStream?.id} newStreamId=${stream.id} shouldRequestAudio=${shouldRequestAudio} shouldRequestVideo=${shouldRequestVideo})`); if (!shouldRequestAudio) { for (const track of stream.getAudioTracks()) { stream.removeTrack(track); } } - if (!shouldRequestVideo) { for (const track of stream.getVideoTracks()) { stream.removeTrack(track); } } } - if (reusable) { this.userMediaStreams.push(stream); } - + this.emit(MediaHandlerEvent.LocalStreamsChanged); return stream; } + /** * Stops all tracks on the provided usermedia stream */ - - stopUserMediaStream(mediaStream) { - _logger.logger.debug("Stopping usermedia stream", mediaStream.id); - + _logger.logger.log(`MediaHandler stopUserMediaStream() stopping (streamId=${mediaStream.id})`); for (const track of mediaStream.getTracks()) { track.stop(); } - const index = this.userMediaStreams.indexOf(mediaStream); - if (index !== -1) { - _logger.logger.debug("Splicing usermedia stream out stream array", mediaStream.id); - + _logger.logger.debug(`MediaHandler stopUserMediaStream() splicing usermedia stream out stream array (streamId=${mediaStream.id})`, mediaStream.id); this.userMediaStreams.splice(index, 1); } - + this.emit(MediaHandlerEvent.LocalStreamsChanged); if (this.localUserMediaStream === mediaStream) { this.localUserMediaStream = undefined; } } + /** - * @param desktopCapturerSourceId sourceId for Electron DesktopCapturer - * @param reusable is allowed to be reused by the MediaHandler - * @returns {MediaStream} based on passed parameters + * @param desktopCapturerSourceId - sourceId for Electron DesktopCapturer + * @param reusable - is allowed to be reused by the MediaHandler + * @returns based on passed parameters */ - - - async getScreensharingStream(desktopCapturerSourceId, reusable = true) { + async getScreensharingStream(opts = {}, reusable = true) { let stream; - if (this.screensharingStreams.length === 0) { - const screenshareConstraints = this.getScreenshareContraints(desktopCapturerSourceId); - if (!screenshareConstraints) return null; - - if (desktopCapturerSourceId) { + const screenshareConstraints = this.getScreenshareContraints(opts); + if (opts.desktopCapturerSourceId) { // We are using Electron - _logger.logger.debug("Getting screensharing stream using getUserMedia()", desktopCapturerSourceId); - + _logger.logger.debug(`MediaHandler getScreensharingStream() calling getUserMedia() (opts=${JSON.stringify(opts)})`); stream = await navigator.mediaDevices.getUserMedia(screenshareConstraints); } else { // We are not using Electron - _logger.logger.debug("Getting screensharing stream using getDisplayMedia()"); - + _logger.logger.debug(`MediaHandler getScreensharingStream() calling getDisplayMedia() (opts=${JSON.stringify(opts)})`); stream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints); } } else { const matchingStream = this.screensharingStreams[this.screensharingStreams.length - 1]; - - _logger.logger.log("Cloning screensharing stream", matchingStream.id); - + _logger.logger.log(`MediaHandler getScreensharingStream() cloning (streamId=${matchingStream.id})`); stream = matchingStream.clone(); } - if (reusable) { this.screensharingStreams.push(stream); } - + this.emit(MediaHandlerEvent.LocalStreamsChanged); return stream; } + /** * Stops all tracks on the provided screensharing stream */ - - stopScreensharingStream(mediaStream) { - _logger.logger.debug("Stopping screensharing stream", mediaStream.id); - + _logger.logger.debug(`MediaHandler stopScreensharingStream() stopping stream (streamId=${mediaStream.id})`); for (const track of mediaStream.getTracks()) { track.stop(); } - const index = this.screensharingStreams.indexOf(mediaStream); - if (index !== -1) { - _logger.logger.debug("Splicing screensharing stream out stream array", mediaStream.id); - + _logger.logger.debug(`MediaHandler stopScreensharingStream() splicing stream out (streamId=${mediaStream.id})`); this.screensharingStreams.splice(index, 1); } + this.emit(MediaHandlerEvent.LocalStreamsChanged); } + /** * Stops all local media tracks */ - - stopAllStreams() { for (const stream of this.userMediaStreams) { + _logger.logger.log(`MediaHandler stopAllStreams() stopping (streamId=${stream.id})`); for (const track of stream.getTracks()) { track.stop(); } } - for (const stream of this.screensharingStreams) { for (const track of stream.getTracks()) { track.stop(); } } - this.userMediaStreams = []; this.screensharingStreams = []; this.localUserMediaStream = undefined; + this.emit(MediaHandlerEvent.LocalStreamsChanged); } - getUserMediaContraints(audio, video) { const isWebkit = !!navigator.webkitGetUserMedia; return { audio: audio ? { deviceId: this.audioInput ? { ideal: this.audioInput + } : undefined, + autoGainControl: this.audioSettings ? { + ideal: this.audioSettings.autoGainControl + } : undefined, + echoCancellation: this.audioSettings ? { + ideal: this.audioSettings.echoCancellation + } : undefined, + noiseSuppression: this.audioSettings ? { + ideal: this.audioSettings.noiseSuppression } : undefined } : false, video: video ? { deviceId: this.videoInput ? { ideal: this.videoInput } : undefined, - /* We want 640x360. Chrome will give it only if we ask exactly, FF refuses entirely if we ask exactly, so have to ask for ideal instead @@ -282,13 +353,14 @@ } : false }; } - - getScreenshareContraints(desktopCapturerSourceId) { + getScreenshareContraints(opts) { + const { + desktopCapturerSourceId, + audio + } = opts; if (desktopCapturerSourceId) { - _logger.logger.debug("Using desktop capturer source", desktopCapturerSourceId); - return { - audio: false, + audio: audio ?? false, video: { mandatory: { chromeMediaSource: "desktop", @@ -297,15 +369,11 @@ } }; } else { - _logger.logger.debug("Not using desktop capturer source"); - return { - audio: false, + audio: audio ?? false, video: true }; } } - } - exports.MediaHandler = MediaHandler; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/ClientWidgetApi.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/ClientWidgetApi.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/ClientWidgetApi.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/ClientWidgetApi.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,1293 @@ +"use strict"; + +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ClientWidgetApi = void 0; + +var _events = require("events"); + +var _PostmessageTransport = require("./transport/PostmessageTransport"); + +var _WidgetApiDirection = require("./interfaces/WidgetApiDirection"); + +var _WidgetApiAction = require("./interfaces/WidgetApiAction"); + +var _Capabilities = require("./interfaces/Capabilities"); + +var _ApiVersion = require("./interfaces/ApiVersion"); + +var _WidgetEventCapability = require("./models/WidgetEventCapability"); + +var _GetOpenIDAction = require("./interfaces/GetOpenIDAction"); + +var _SimpleObservable = require("./util/SimpleObservable"); + +var _Symbols = require("./Symbols"); + +function _regeneratorRuntime() { "use strict"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime = function _regeneratorRuntime() { return exports; }; var exports = {}, Op = Object.prototype, hasOwn = Op.hasOwnProperty, $Symbol = "function" == typeof Symbol ? Symbol : {}, iteratorSymbol = $Symbol.iterator || "@@iterator", asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator", toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; function define(obj, key, value) { return Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }), obj[key]; } try { define({}, ""); } catch (err) { define = function define(obj, key, value) { return obj[key] = value; }; } function wrap(innerFn, outerFn, self, tryLocsList) { var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator, generator = Object.create(protoGenerator.prototype), context = new Context(tryLocsList || []); return generator._invoke = function (innerFn, self, context) { var state = "suspendedStart"; return function (method, arg) { if ("executing" === state) throw new Error("Generator is already running"); if ("completed" === state) { if ("throw" === method) throw arg; return doneResult(); } for (context.method = method, context.arg = arg;;) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if ("next" === context.method) context.sent = context._sent = context.arg;else if ("throw" === context.method) { if ("suspendedStart" === state) throw state = "completed", context.arg; context.dispatchException(context.arg); } else "return" === context.method && context.abrupt("return", context.arg); state = "executing"; var record = tryCatch(innerFn, self, context); if ("normal" === record.type) { if (state = context.done ? "completed" : "suspendedYield", record.arg === ContinueSentinel) continue; return { value: record.arg, done: context.done }; } "throw" === record.type && (state = "completed", context.method = "throw", context.arg = record.arg); } }; }(innerFn, self, context), generator; } function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } exports.wrap = wrap; var ContinueSentinel = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} var IteratorPrototype = {}; define(IteratorPrototype, iteratorSymbol, function () { return this; }); var getProto = Object.getPrototypeOf, NativeIteratorPrototype = getProto && getProto(getProto(values([]))); NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol) && (IteratorPrototype = NativeIteratorPrototype); var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function (method) { define(prototype, method, function (arg) { return this._invoke(method, arg); }); }); } function AsyncIterator(generator, PromiseImpl) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if ("throw" !== record.type) { var result = record.arg, value = result.value; return value && "object" == _typeof(value) && hasOwn.call(value, "__await") ? PromiseImpl.resolve(value.__await).then(function (value) { invoke("next", value, resolve, reject); }, function (err) { invoke("throw", err, resolve, reject); }) : PromiseImpl.resolve(value).then(function (unwrapped) { result.value = unwrapped, resolve(result); }, function (error) { return invoke("throw", error, resolve, reject); }); } reject(record.arg); } var previousPromise; this._invoke = function (method, arg) { function callInvokeWithMethodAndArg() { return new PromiseImpl(function (resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = previousPromise ? previousPromise.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); }; } function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (undefined === method) { if (context.delegate = null, "throw" === context.method) { if (delegate.iterator["return"] && (context.method = "return", context.arg = undefined, maybeInvokeDelegate(delegate, context), "throw" === context.method)) return ContinueSentinel; context.method = "throw", context.arg = new TypeError("The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if ("throw" === record.type) return context.method = "throw", context.arg = record.arg, context.delegate = null, ContinueSentinel; var info = record.arg; return info ? info.done ? (context[delegate.resultName] = info.value, context.next = delegate.nextLoc, "return" !== context.method && (context.method = "next", context.arg = undefined), context.delegate = null, ContinueSentinel) : info : (context.method = "throw", context.arg = new TypeError("iterator result is not an object"), context.delegate = null, ContinueSentinel); } function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; 1 in locs && (entry.catchLoc = locs[1]), 2 in locs && (entry.finallyLoc = locs[2], entry.afterLoc = locs[3]), this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal", delete record.arg, entry.completion = record; } function Context(tryLocsList) { this.tryEntries = [{ tryLoc: "root" }], tryLocsList.forEach(pushTryEntry, this), this.reset(!0); } function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) return iteratorMethod.call(iterable); if ("function" == typeof iterable.next) return iterable; if (!isNaN(iterable.length)) { var i = -1, next = function next() { for (; ++i < iterable.length;) { if (hasOwn.call(iterable, i)) return next.value = iterable[i], next.done = !1, next; } return next.value = undefined, next.done = !0, next; }; return next.next = next; } } return { next: doneResult }; } function doneResult() { return { value: undefined, done: !0 }; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, define(Gp, "constructor", GeneratorFunctionPrototype), define(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, toStringTagSymbol, "GeneratorFunction"), exports.isGeneratorFunction = function (genFun) { var ctor = "function" == typeof genFun && genFun.constructor; return !!ctor && (ctor === GeneratorFunction || "GeneratorFunction" === (ctor.displayName || ctor.name)); }, exports.mark = function (genFun) { return Object.setPrototypeOf ? Object.setPrototypeOf(genFun, GeneratorFunctionPrototype) : (genFun.__proto__ = GeneratorFunctionPrototype, define(genFun, toStringTagSymbol, "GeneratorFunction")), genFun.prototype = Object.create(Gp), genFun; }, exports.awrap = function (arg) { return { __await: arg }; }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, asyncIteratorSymbol, function () { return this; }), exports.AsyncIterator = AsyncIterator, exports.async = function (innerFn, outerFn, self, tryLocsList, PromiseImpl) { void 0 === PromiseImpl && (PromiseImpl = Promise); var iter = new AsyncIterator(wrap(innerFn, outerFn, self, tryLocsList), PromiseImpl); return exports.isGeneratorFunction(outerFn) ? iter : iter.next().then(function (result) { return result.done ? result.value : iter.next(); }); }, defineIteratorMethods(Gp), define(Gp, toStringTagSymbol, "Generator"), define(Gp, iteratorSymbol, function () { return this; }), define(Gp, "toString", function () { return "[object Generator]"; }), exports.keys = function (object) { var keys = []; for (var key in object) { keys.push(key); } return keys.reverse(), function next() { for (; keys.length;) { var key = keys.pop(); if (key in object) return next.value = key, next.done = !1, next; } return next.done = !0, next; }; }, exports.values = values, Context.prototype = { constructor: Context, reset: function reset(skipTempReset) { if (this.prev = 0, this.next = 0, this.sent = this._sent = undefined, this.done = !1, this.delegate = null, this.method = "next", this.arg = undefined, this.tryEntries.forEach(resetTryEntry), !skipTempReset) for (var name in this) { "t" === name.charAt(0) && hasOwn.call(this, name) && !isNaN(+name.slice(1)) && (this[name] = undefined); } }, stop: function stop() { this.done = !0; var rootRecord = this.tryEntries[0].completion; if ("throw" === rootRecord.type) throw rootRecord.arg; return this.rval; }, dispatchException: function dispatchException(exception) { if (this.done) throw exception; var context = this; function handle(loc, caught) { return record.type = "throw", record.arg = exception, context.next = loc, caught && (context.method = "next", context.arg = undefined), !!caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i], record = entry.completion; if ("root" === entry.tryLoc) return handle("end"); if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"), hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) return handle(entry.catchLoc, !0); if (this.prev < entry.finallyLoc) return handle(entry.finallyLoc); } else if (hasCatch) { if (this.prev < entry.catchLoc) return handle(entry.catchLoc, !0); } else { if (!hasFinally) throw new Error("try statement without catch or finally"); if (this.prev < entry.finallyLoc) return handle(entry.finallyLoc); } } } }, abrupt: function abrupt(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } finallyEntry && ("break" === type || "continue" === type) && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc && (finallyEntry = null); var record = finallyEntry ? finallyEntry.completion : {}; return record.type = type, record.arg = arg, finallyEntry ? (this.method = "next", this.next = finallyEntry.finallyLoc, ContinueSentinel) : this.complete(record); }, complete: function complete(record, afterLoc) { if ("throw" === record.type) throw record.arg; return "break" === record.type || "continue" === record.type ? this.next = record.arg : "return" === record.type ? (this.rval = this.arg = record.arg, this.method = "return", this.next = "end") : "normal" === record.type && afterLoc && (this.next = afterLoc), ContinueSentinel; }, finish: function finish(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) return this.complete(entry.completion, entry.afterLoc), resetTryEntry(entry), ContinueSentinel; } }, "catch": function _catch(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if ("throw" === record.type) { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } throw new Error("illegal catch attempt"); }, delegateYield: function delegateYield(iterable, resultName, nextLoc) { return this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }, "next" === this.method && (this.arg = undefined), ContinueSentinel; } }, exports; } + +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } + +function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } + +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } + +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } + +function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } + +function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } + +function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } + +function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } + +function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } + +function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _asyncIterator(iterable) { var method, async, sync, retry = 2; for ("undefined" != typeof Symbol && (async = Symbol.asyncIterator, sync = Symbol.iterator); retry--;) { if (async && null != (method = iterable[async])) return method.call(iterable); if (sync && null != (method = iterable[sync])) return new AsyncFromSyncIterator(method.call(iterable)); async = "@@asyncIterator", sync = "@@iterator"; } throw new TypeError("Object is not async iterable"); } + +function AsyncFromSyncIterator(s) { function AsyncFromSyncIteratorContinuation(r) { if (Object(r) !== r) return Promise.reject(new TypeError(r + " is not an object.")); var done = r.done; return Promise.resolve(r.value).then(function (value) { return { value: value, done: done }; }); } return AsyncFromSyncIterator = function AsyncFromSyncIterator(s) { this.s = s, this.n = s.next; }, AsyncFromSyncIterator.prototype = { s: null, n: null, next: function next() { return AsyncFromSyncIteratorContinuation(this.n.apply(this.s, arguments)); }, "return": function _return(value) { var ret = this.s["return"]; return void 0 === ret ? Promise.resolve({ value: value, done: !0 }) : AsyncFromSyncIteratorContinuation(ret.apply(this.s, arguments)); }, "throw": function _throw(value) { var thr = this.s["return"]; return void 0 === thr ? Promise.reject(value) : AsyncFromSyncIteratorContinuation(thr.apply(this.s, arguments)); } }, new AsyncFromSyncIterator(s); } + +/** + * API handler for the client side of widgets. This raises events + * for each action received as `action:${action}` (eg: "action:screenshot"). + * Default handling can be prevented by using preventDefault() on the + * raised event. The default handling varies for each action: ones + * which the SDK can handle safely are acknowledged appropriately and + * ones which are unhandled (custom or require the client to do something) + * are rejected with an error. + * + * Events which are preventDefault()ed must reply using the transport. + * The events raised will have a default of an IWidgetApiRequest + * interface. + * + * When the ClientWidgetApi is ready to start sending requests, it will + * raise a "ready" CustomEvent. After the ready event fires, actions can + * be sent and the transport will be ready. + * + * When the widget has indicated it has loaded, this class raises a + * "preparing" CustomEvent. The preparing event does not indicate that + * the widget is ready to receive communications - that is signified by + * the ready event exclusively. + * + * This class only handles one widget at a time. + */ +var ClientWidgetApi = /*#__PURE__*/function (_EventEmitter) { + _inherits(ClientWidgetApi, _EventEmitter); + + var _super = _createSuper(ClientWidgetApi); + + // contentLoadedActionSent is used to check that only one ContentLoaded request is send. + + /** + * Creates a new client widget API. This will instantiate the transport + * and start everything. When the iframe is loaded under the widget's + * conditions, a "ready" event will be raised. + * @param {Widget} widget The widget to communicate with. + * @param {HTMLIFrameElement} iframe The iframe the widget is in. + * @param {WidgetDriver} driver The driver for this widget/client. + */ + function ClientWidgetApi(widget, iframe, driver) { + var _this; + + _classCallCheck(this, ClientWidgetApi); + + _this = _super.call(this); + _this.widget = widget; + _this.iframe = iframe; + _this.driver = driver; + + _defineProperty(_assertThisInitialized(_this), "transport", void 0); + + _defineProperty(_assertThisInitialized(_this), "contentLoadedActionSent", false); + + _defineProperty(_assertThisInitialized(_this), "allowedCapabilities", new Set()); + + _defineProperty(_assertThisInitialized(_this), "allowedEvents", []); + + _defineProperty(_assertThisInitialized(_this), "isStopped", false); + + _defineProperty(_assertThisInitialized(_this), "turnServers", null); + + if (!(iframe !== null && iframe !== void 0 && iframe.contentWindow)) { + throw new Error("No iframe supplied"); + } + + if (!widget) { + throw new Error("Invalid widget"); + } + + if (!driver) { + throw new Error("Invalid driver"); + } + + _this.transport = new _PostmessageTransport.PostmessageTransport(_WidgetApiDirection.WidgetApiDirection.ToWidget, widget.id, iframe.contentWindow, window); + _this.transport.targetOrigin = widget.origin; + + _this.transport.on("message", _this.handleMessage.bind(_assertThisInitialized(_this))); + + iframe.addEventListener("load", _this.onIframeLoad.bind(_assertThisInitialized(_this))); + + _this.transport.start(); + + return _this; + } + + _createClass(ClientWidgetApi, [{ + key: "hasCapability", + value: function hasCapability(capability) { + return this.allowedCapabilities.has(capability); + } + }, { + key: "canUseRoomTimeline", + value: function canUseRoomTimeline(roomId) { + return this.hasCapability("org.matrix.msc2762.timeline:".concat(_Symbols.Symbols.AnyRoom)) || this.hasCapability("org.matrix.msc2762.timeline:".concat(roomId)); + } + }, { + key: "canSendRoomEvent", + value: function canSendRoomEvent(eventType) { + var msgtype = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + return this.allowedEvents.some(function (e) { + return e.matchesAsRoomEvent(_WidgetEventCapability.EventDirection.Send, eventType, msgtype); + }); + } + }, { + key: "canSendStateEvent", + value: function canSendStateEvent(eventType, stateKey) { + return this.allowedEvents.some(function (e) { + return e.matchesAsStateEvent(_WidgetEventCapability.EventDirection.Send, eventType, stateKey); + }); + } + }, { + key: "canSendToDeviceEvent", + value: function canSendToDeviceEvent(eventType) { + return this.allowedEvents.some(function (e) { + return e.matchesAsToDeviceEvent(_WidgetEventCapability.EventDirection.Send, eventType); + }); + } + }, { + key: "canReceiveRoomEvent", + value: function canReceiveRoomEvent(eventType) { + var msgtype = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + return this.allowedEvents.some(function (e) { + return e.matchesAsRoomEvent(_WidgetEventCapability.EventDirection.Receive, eventType, msgtype); + }); + } + }, { + key: "canReceiveStateEvent", + value: function canReceiveStateEvent(eventType, stateKey) { + return this.allowedEvents.some(function (e) { + return e.matchesAsStateEvent(_WidgetEventCapability.EventDirection.Receive, eventType, stateKey); + }); + } + }, { + key: "canReceiveToDeviceEvent", + value: function canReceiveToDeviceEvent(eventType) { + return this.allowedEvents.some(function (e) { + return e.matchesAsToDeviceEvent(_WidgetEventCapability.EventDirection.Receive, eventType); + }); + } + }, { + key: "stop", + value: function stop() { + this.isStopped = true; + this.transport.stop(); + } + }, { + key: "beginCapabilities", + value: function beginCapabilities() { + var _this2 = this; + + // widget has loaded - tell all the listeners that + this.emit("preparing"); + var requestedCaps; + this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.Capabilities, {}).then(function (caps) { + requestedCaps = caps.capabilities; + return _this2.driver.validateCapabilities(new Set(caps.capabilities)); + }).then(function (allowedCaps) { + console.log("Widget ".concat(_this2.widget.id, " is allowed capabilities:"), Array.from(allowedCaps)); + _this2.allowedCapabilities = allowedCaps; + _this2.allowedEvents = _WidgetEventCapability.WidgetEventCapability.findEventCapabilities(allowedCaps); + + _this2.notifyCapabilities(requestedCaps); + + _this2.emit("ready"); + }); + } + }, { + key: "notifyCapabilities", + value: function notifyCapabilities(requested) { + var _this3 = this; + + this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.NotifyCapabilities, { + requested: requested, + approved: Array.from(this.allowedCapabilities) + })["catch"](function (e) { + console.warn("non-fatal error notifying widget of approved capabilities:", e); + }).then(function () { + _this3.emit("capabilitiesNotified"); + }); + } + }, { + key: "onIframeLoad", + value: function onIframeLoad(ev) { + if (this.widget.waitForIframeLoad) { + // If the widget is set to waitForIframeLoad the capabilities immediatly get setup after load. + // The client does not wait for the ContentLoaded action. + this.beginCapabilities(); + } else { + // Reaching this means, that the Iframe got reloaded/loaded and + // the clientApi is awaiting the FIRST ContentLoaded action. + this.contentLoadedActionSent = false; + } + } + }, { + key: "handleContentLoadedAction", + value: function handleContentLoadedAction(action) { + if (this.contentLoadedActionSent) { + throw new Error("Improper sequence: ContentLoaded Action can only be send once after the widget loaded " + "and should only be used if waitForIframeLoad is false (default=true)"); + } + + if (this.widget.waitForIframeLoad) { + this.transport.reply(action, { + error: { + message: "Improper sequence: not expecting ContentLoaded event if " + "waitForIframLoad is true (default=true)" + } + }); + } else { + this.transport.reply(action, {}); + this.beginCapabilities(); + } + + this.contentLoadedActionSent = true; + } + }, { + key: "replyVersions", + value: function replyVersions(request) { + this.transport.reply(request, { + supported_versions: _ApiVersion.CurrentApiVersions + }); + } + }, { + key: "handleCapabilitiesRenegotiate", + value: function handleCapabilitiesRenegotiate(request) { + var _request$data, + _this4 = this; + + // acknowledge first + this.transport.reply(request, {}); + var requested = ((_request$data = request.data) === null || _request$data === void 0 ? void 0 : _request$data.capabilities) || []; + var newlyRequested = new Set(requested.filter(function (r) { + return !_this4.hasCapability(r); + })); + + if (newlyRequested.size === 0) { + // Nothing to do - notify capabilities + return this.notifyCapabilities([]); + } + + this.driver.validateCapabilities(newlyRequested).then(function (allowed) { + allowed.forEach(function (c) { + return _this4.allowedCapabilities.add(c); + }); + + var allowedEvents = _WidgetEventCapability.WidgetEventCapability.findEventCapabilities(allowed); + + allowedEvents.forEach(function (c) { + return _this4.allowedEvents.push(c); + }); + return _this4.notifyCapabilities(Array.from(newlyRequested)); + }); + } + }, { + key: "handleNavigate", + value: function handleNavigate(request) { + var _request$data2, + _request$data3, + _this5 = this; + + if (!this.hasCapability(_Capabilities.MatrixCapabilities.MSC2931Navigate)) { + return this.transport.reply(request, { + error: { + message: "Missing capability" + } + }); + } + + if (!((_request$data2 = request.data) !== null && _request$data2 !== void 0 && _request$data2.uri) || !((_request$data3 = request.data) !== null && _request$data3 !== void 0 && _request$data3.uri.toString().startsWith("https://matrix.to/#"))) { + return this.transport.reply(request, { + error: { + message: "Invalid matrix.to URI" + } + }); + } + + var onErr = function onErr(e) { + console.error("[ClientWidgetApi] Failed to handle navigation: ", e); + return _this5.transport.reply(request, { + error: { + message: "Error handling navigation" + } + }); + }; + + try { + this.driver.navigate(request.data.uri.toString())["catch"](function (e) { + return onErr(e); + }).then(function () { + return _this5.transport.reply(request, {}); + }); + } catch (e) { + return onErr(e); + } + } + }, { + key: "handleOIDC", + value: function handleOIDC(request) { + var _this6 = this; + + var phase = 1; // 1 = initial request, 2 = after user manual confirmation + + var replyState = function replyState(state, credential) { + credential = credential || {}; + + if (phase > 1) { + return _this6.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.OpenIDCredentials, _objectSpread({ + state: state, + original_request_id: request.requestId + }, credential)); + } else { + return _this6.transport.reply(request, _objectSpread({ + state: state + }, credential)); + } + }; + + var replyError = function replyError(msg) { + console.error("[ClientWidgetApi] Failed to handle OIDC: ", msg); + + if (phase > 1) { + // We don't have a way to indicate that a random error happened in this flow, so + // just block the attempt. + return replyState(_GetOpenIDAction.OpenIDRequestState.Blocked); + } else { + return _this6.transport.reply(request, { + error: { + message: msg + } + }); + } + }; + + var observer = new _SimpleObservable.SimpleObservable(function (update) { + if (update.state === _GetOpenIDAction.OpenIDRequestState.PendingUserConfirmation && phase > 1) { + observer.close(); + return replyError("client provided out-of-phase response to OIDC flow"); + } + + if (update.state === _GetOpenIDAction.OpenIDRequestState.PendingUserConfirmation) { + replyState(update.state); + phase++; + return; + } + + if (update.state === _GetOpenIDAction.OpenIDRequestState.Allowed && !update.token) { + return replyError("client provided invalid OIDC token for an allowed request"); + } + + if (update.state === _GetOpenIDAction.OpenIDRequestState.Blocked) { + update.token = null; // just in case the client did something weird + } + + observer.close(); + return replyState(update.state, update.token); + }); + this.driver.askOpenID(observer); + } + }, { + key: "handleReadEvents", + value: function handleReadEvents(request) { + var _this7 = this; + + if (!request.data.type) { + return this.transport.reply(request, { + error: { + message: "Invalid request - missing event type" + } + }); + } + + if (request.data.limit !== undefined && (!request.data.limit || request.data.limit < 0)) { + return this.transport.reply(request, { + error: { + message: "Invalid request - limit out of range" + } + }); + } + + var askRoomIds = null; // null denotes current room only + + if (request.data.room_ids) { + askRoomIds = request.data.room_ids; + + if (!Array.isArray(askRoomIds)) { + askRoomIds = [askRoomIds]; + } + + var _iterator2 = _createForOfIteratorHelper(askRoomIds), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var roomId = _step2.value; + + if (!this.canUseRoomTimeline(roomId)) { + return this.transport.reply(request, { + error: { + message: "Unable to access room timeline: ".concat(roomId) + } + }); + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + } + + var limit = request.data.limit || 0; + var events = Promise.resolve([]); + + if (request.data.state_key !== undefined) { + var stateKey = request.data.state_key === true ? undefined : request.data.state_key.toString(); + + if (!this.canReceiveStateEvent(request.data.type, stateKey)) { + return this.transport.reply(request, { + error: { + message: "Cannot read state events of this type" + } + }); + } + + events = this.driver.readStateEvents(request.data.type, stateKey, limit, askRoomIds); + } else { + if (!this.canReceiveRoomEvent(request.data.type, request.data.msgtype)) { + return this.transport.reply(request, { + error: { + message: "Cannot read room events of this type" + } + }); + } + + events = this.driver.readRoomEvents(request.data.type, request.data.msgtype, limit, askRoomIds); + } + + return events.then(function (evs) { + return _this7.transport.reply(request, { + events: evs + }); + }); + } + }, { + key: "handleSendEvent", + value: function handleSendEvent(request) { + var _this8 = this; + + if (!request.data.type) { + return this.transport.reply(request, { + error: { + message: "Invalid request - missing event type" + } + }); + } + + if (!!request.data.room_id && !this.canUseRoomTimeline(request.data.room_id)) { + return this.transport.reply(request, { + error: { + message: "Unable to access room timeline: ".concat(request.data.room_id) + } + }); + } + + var isState = request.data.state_key !== null && request.data.state_key !== undefined; + var sendEventPromise; + + if (isState) { + if (!this.canSendStateEvent(request.data.type, request.data.state_key)) { + return this.transport.reply(request, { + error: { + message: "Cannot send state events of this type" + } + }); + } + + sendEventPromise = this.driver.sendEvent(request.data.type, request.data.content || {}, request.data.state_key, request.data.room_id); + } else { + var content = request.data.content || {}; + var msgtype = content['msgtype']; + + if (!this.canSendRoomEvent(request.data.type, msgtype)) { + return this.transport.reply(request, { + error: { + message: "Cannot send room events of this type" + } + }); + } + + sendEventPromise = this.driver.sendEvent(request.data.type, content, null, // not sending a state event + request.data.room_id); + } + + sendEventPromise.then(function (sentEvent) { + return _this8.transport.reply(request, { + room_id: sentEvent.roomId, + event_id: sentEvent.eventId + }); + })["catch"](function (e) { + console.error("error sending event: ", e); + return _this8.transport.reply(request, { + error: { + message: "Error sending event" + } + }); + }); + } + }, { + key: "handleSendToDevice", + value: function () { + var _handleSendToDevice = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(request) { + return _regeneratorRuntime().wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + if (request.data.type) { + _context.next = 5; + break; + } + + _context.next = 3; + return this.transport.reply(request, { + error: { + message: "Invalid request - missing event type" + } + }); + + case 3: + _context.next = 32; + break; + + case 5: + if (request.data.messages) { + _context.next = 10; + break; + } + + _context.next = 8; + return this.transport.reply(request, { + error: { + message: "Invalid request - missing event contents" + } + }); + + case 8: + _context.next = 32; + break; + + case 10: + if (!(typeof request.data.encrypted !== "boolean")) { + _context.next = 15; + break; + } + + _context.next = 13; + return this.transport.reply(request, { + error: { + message: "Invalid request - missing encryption flag" + } + }); + + case 13: + _context.next = 32; + break; + + case 15: + if (this.canSendToDeviceEvent(request.data.type)) { + _context.next = 20; + break; + } + + _context.next = 18; + return this.transport.reply(request, { + error: { + message: "Cannot send to-device events of this type" + } + }); + + case 18: + _context.next = 32; + break; + + case 20: + _context.prev = 20; + _context.next = 23; + return this.driver.sendToDevice(request.data.type, request.data.encrypted, request.data.messages); + + case 23: + _context.next = 25; + return this.transport.reply(request, {}); + + case 25: + _context.next = 32; + break; + + case 27: + _context.prev = 27; + _context.t0 = _context["catch"](20); + console.error("error sending to-device event", _context.t0); + _context.next = 32; + return this.transport.reply(request, { + error: { + message: "Error sending event" + } + }); + + case 32: + case "end": + return _context.stop(); + } + } + }, _callee, this, [[20, 27]]); + })); + + function handleSendToDevice(_x) { + return _handleSendToDevice.apply(this, arguments); + } + + return handleSendToDevice; + }() + }, { + key: "pollTurnServers", + value: function () { + var _pollTurnServers = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(turnServers, initialServer) { + var _iteratorAbruptCompletion, _didIteratorError, _iteratorError, _iterator, _step, server; + + return _regeneratorRuntime().wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + _context2.prev = 0; + _context2.next = 3; + return this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.UpdateTurnServers, initialServer // it's compatible, but missing the index signature + ); + + case 3: + // Pick the generator up where we left off + _iteratorAbruptCompletion = false; + _didIteratorError = false; + _context2.prev = 5; + _iterator = _asyncIterator(turnServers); + + case 7: + _context2.next = 9; + return _iterator.next(); + + case 9: + if (!(_iteratorAbruptCompletion = !(_step = _context2.sent).done)) { + _context2.next = 16; + break; + } + + server = _step.value; + _context2.next = 13; + return this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.UpdateTurnServers, server // it's compatible, but missing the index signature + ); + + case 13: + _iteratorAbruptCompletion = false; + _context2.next = 7; + break; + + case 16: + _context2.next = 22; + break; + + case 18: + _context2.prev = 18; + _context2.t0 = _context2["catch"](5); + _didIteratorError = true; + _iteratorError = _context2.t0; + + case 22: + _context2.prev = 22; + _context2.prev = 23; + + if (!(_iteratorAbruptCompletion && _iterator["return"] != null)) { + _context2.next = 27; + break; + } + + _context2.next = 27; + return _iterator["return"](); + + case 27: + _context2.prev = 27; + + if (!_didIteratorError) { + _context2.next = 30; + break; + } + + throw _iteratorError; + + case 30: + return _context2.finish(27); + + case 31: + return _context2.finish(22); + + case 32: + _context2.next = 37; + break; + + case 34: + _context2.prev = 34; + _context2.t1 = _context2["catch"](0); + console.error("error polling for TURN servers", _context2.t1); + + case 37: + case "end": + return _context2.stop(); + } + } + }, _callee2, this, [[0, 34], [5, 18, 22, 32], [23,, 27, 31]]); + })); + + function pollTurnServers(_x2, _x3) { + return _pollTurnServers.apply(this, arguments); + } + + return pollTurnServers; + }() + }, { + key: "handleWatchTurnServers", + value: function () { + var _handleWatchTurnServers = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(request) { + var turnServers, _yield$turnServers$ne, done, value; + + return _regeneratorRuntime().wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + if (this.hasCapability(_Capabilities.MatrixCapabilities.MSC3846TurnServers)) { + _context3.next = 5; + break; + } + + _context3.next = 3; + return this.transport.reply(request, { + error: { + message: "Missing capability" + } + }); + + case 3: + _context3.next = 30; + break; + + case 5: + if (!this.turnServers) { + _context3.next = 10; + break; + } + + _context3.next = 8; + return this.transport.reply(request, {}); + + case 8: + _context3.next = 30; + break; + + case 10: + _context3.prev = 10; + turnServers = this.driver.getTurnServers(); // Peek at the first result, so we can at least verify that the + // client isn't banned from getting TURN servers entirely + + _context3.next = 14; + return turnServers.next(); + + case 14: + _yield$turnServers$ne = _context3.sent; + done = _yield$turnServers$ne.done; + value = _yield$turnServers$ne.value; + + if (!done) { + _context3.next = 19; + break; + } + + throw new Error("Client refuses to provide any TURN servers"); + + case 19: + _context3.next = 21; + return this.transport.reply(request, {}); + + case 21: + // Start the poll loop, sending the widget the initial result + this.pollTurnServers(turnServers, value); + this.turnServers = turnServers; + _context3.next = 30; + break; + + case 25: + _context3.prev = 25; + _context3.t0 = _context3["catch"](10); + console.error("error getting first TURN server results", _context3.t0); + _context3.next = 30; + return this.transport.reply(request, { + error: { + message: "TURN servers not available" + } + }); + + case 30: + case "end": + return _context3.stop(); + } + } + }, _callee3, this, [[10, 25]]); + })); + + function handleWatchTurnServers(_x4) { + return _handleWatchTurnServers.apply(this, arguments); + } + + return handleWatchTurnServers; + }() + }, { + key: "handleUnwatchTurnServers", + value: function () { + var _handleUnwatchTurnServers = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(request) { + return _regeneratorRuntime().wrap(function _callee4$(_context4) { + while (1) { + switch (_context4.prev = _context4.next) { + case 0: + if (this.hasCapability(_Capabilities.MatrixCapabilities.MSC3846TurnServers)) { + _context4.next = 5; + break; + } + + _context4.next = 3; + return this.transport.reply(request, { + error: { + message: "Missing capability" + } + }); + + case 3: + _context4.next = 15; + break; + + case 5: + if (this.turnServers) { + _context4.next = 10; + break; + } + + _context4.next = 8; + return this.transport.reply(request, {}); + + case 8: + _context4.next = 15; + break; + + case 10: + _context4.next = 12; + return this.turnServers["return"](undefined); + + case 12: + this.turnServers = null; + _context4.next = 15; + return this.transport.reply(request, {}); + + case 15: + case "end": + return _context4.stop(); + } + } + }, _callee4, this); + })); + + function handleUnwatchTurnServers(_x5) { + return _handleUnwatchTurnServers.apply(this, arguments); + } + + return handleUnwatchTurnServers; + }() + }, { + key: "handleReadRelations", + value: function () { + var _handleReadRelations = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(request) { + var _this9 = this; + + var result, chunk; + return _regeneratorRuntime().wrap(function _callee5$(_context5) { + while (1) { + switch (_context5.prev = _context5.next) { + case 0: + if (request.data.event_id) { + _context5.next = 2; + break; + } + + return _context5.abrupt("return", this.transport.reply(request, { + error: { + message: "Invalid request - missing event ID" + } + })); + + case 2: + if (!(request.data.limit !== undefined && request.data.limit < 0)) { + _context5.next = 4; + break; + } + + return _context5.abrupt("return", this.transport.reply(request, { + error: { + message: "Invalid request - limit out of range" + } + })); + + case 4: + if (!(request.data.room_id !== undefined && !this.canUseRoomTimeline(request.data.room_id))) { + _context5.next = 6; + break; + } + + return _context5.abrupt("return", this.transport.reply(request, { + error: { + message: "Unable to access room timeline: ".concat(request.data.room_id) + } + })); + + case 6: + _context5.prev = 6; + _context5.next = 9; + return this.driver.readEventRelations(request.data.event_id, request.data.room_id, request.data.rel_type, request.data.event_type, request.data.from, request.data.to, request.data.limit, request.data.direction); + + case 9: + result = _context5.sent; + + if (!result.originalEvent) { + _context5.next = 18; + break; + } + + if (!(result.originalEvent.state_key !== undefined)) { + _context5.next = 16; + break; + } + + if (this.canReceiveStateEvent(result.originalEvent.type, result.originalEvent.state_key)) { + _context5.next = 14; + break; + } + + return _context5.abrupt("return", this.transport.reply(request, { + error: { + message: "Cannot read state events of this type" + } + })); + + case 14: + _context5.next = 18; + break; + + case 16: + if (this.canReceiveRoomEvent(result.originalEvent.type, result.originalEvent.content['msgtype'])) { + _context5.next = 18; + break; + } + + return _context5.abrupt("return", this.transport.reply(request, { + error: { + message: "Cannot read room events of this type" + } + })); + + case 18: + // only return events that the user has the permission to receive + chunk = result.chunk.filter(function (e) { + if (e.state_key !== undefined) { + return _this9.canReceiveStateEvent(e.type, e.state_key); + } else { + return _this9.canReceiveRoomEvent(e.type, e.content['msgtype']); + } + }); + return _context5.abrupt("return", this.transport.reply(request, { + original_event: result.originalEvent, + chunk: chunk, + prev_batch: result.prevBatch, + next_batch: result.nextBatch + })); + + case 22: + _context5.prev = 22; + _context5.t0 = _context5["catch"](6); + console.error("error getting the relations", _context5.t0); + _context5.next = 27; + return this.transport.reply(request, { + error: { + message: "Unexpected error while reading relations" + } + }); + + case 27: + case "end": + return _context5.stop(); + } + } + }, _callee5, this, [[6, 22]]); + })); + + function handleReadRelations(_x6) { + return _handleReadRelations.apply(this, arguments); + } + + return handleReadRelations; + }() + }, { + key: "handleMessage", + value: function handleMessage(ev) { + if (this.isStopped) return; + var actionEv = new CustomEvent("action:".concat(ev.detail.action), { + detail: ev.detail, + cancelable: true + }); + this.emit("action:".concat(ev.detail.action), actionEv); + + if (!actionEv.defaultPrevented) { + switch (ev.detail.action) { + case _WidgetApiAction.WidgetApiFromWidgetAction.ContentLoaded: + return this.handleContentLoadedAction(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.SupportedApiVersions: + return this.replyVersions(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.SendEvent: + return this.handleSendEvent(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.SendToDevice: + return this.handleSendToDevice(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.GetOpenIDCredentials: + return this.handleOIDC(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.MSC2931Navigate: + return this.handleNavigate(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities: + return this.handleCapabilitiesRenegotiate(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.MSC2876ReadEvents: + return this.handleReadEvents(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.WatchTurnServers: + return this.handleWatchTurnServers(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.UnwatchTurnServers: + return this.handleUnwatchTurnServers(ev.detail); + + case _WidgetApiAction.WidgetApiFromWidgetAction.MSC3869ReadRelations: + return this.handleReadRelations(ev.detail); + + default: + return this.transport.reply(ev.detail, { + error: { + message: "Unknown or unsupported action: " + ev.detail.action + } + }); + } + } + } + /** + * Takes a screenshot of the widget. + * @returns Resolves to the widget's screenshot. + * @throws Throws if there is a problem. + */ + + }, { + key: "takeScreenshot", + value: function takeScreenshot() { + return this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.TakeScreenshot, {}); + } + /** + * Alerts the widget to whether or not it is currently visible. + * @param {boolean} isVisible Whether the widget is visible or not. + * @returns {Promise} Resolves when the widget acknowledges the update. + */ + + }, { + key: "updateVisibility", + value: function updateVisibility(isVisible) { + return this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.UpdateVisibility, { + visible: isVisible + }); + } + }, { + key: "sendWidgetConfig", + value: function sendWidgetConfig(data) { + return this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.WidgetConfig, data).then(); + } + }, { + key: "notifyModalWidgetButtonClicked", + value: function notifyModalWidgetButtonClicked(id) { + return this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.ButtonClicked, { + id: id + }).then(); + } + }, { + key: "notifyModalWidgetClose", + value: function notifyModalWidgetClose(data) { + return this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.CloseModalWidget, data).then(); + } + /** + * Feeds an event to the widget. If the widget is not able to accept the event due to + * permissions, this will no-op and return calmly. If the widget failed to handle the + * event, this will raise an error. + * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. + * @param {string} currentViewedRoomId The room ID the user is currently interacting with. + * Not the room ID of the event. + * @returns {Promise} Resolves when complete, rejects if there was an error sending. + */ + + }, { + key: "feedEvent", + value: function () { + var _feedEvent = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee6(rawEvent, currentViewedRoomId) { + var _rawEvent$content; + + return _regeneratorRuntime().wrap(function _callee6$(_context6) { + while (1) { + switch (_context6.prev = _context6.next) { + case 0: + if (!(rawEvent.room_id !== currentViewedRoomId && !this.canUseRoomTimeline(rawEvent.room_id))) { + _context6.next = 2; + break; + } + + return _context6.abrupt("return"); + + case 2: + if (!(rawEvent.state_key !== undefined && rawEvent.state_key !== null)) { + _context6.next = 7; + break; + } + + if (this.canReceiveStateEvent(rawEvent.type, rawEvent.state_key)) { + _context6.next = 5; + break; + } + + return _context6.abrupt("return"); + + case 5: + _context6.next = 9; + break; + + case 7: + if (this.canReceiveRoomEvent(rawEvent.type, (_rawEvent$content = rawEvent.content) === null || _rawEvent$content === void 0 ? void 0 : _rawEvent$content["msgtype"])) { + _context6.next = 9; + break; + } + + return _context6.abrupt("return"); + + case 9: + _context6.next = 11; + return this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.SendEvent, rawEvent // it's compatible, but missing the index signature + ); + + case 11: + case "end": + return _context6.stop(); + } + } + }, _callee6, this); + })); + + function feedEvent(_x7, _x8) { + return _feedEvent.apply(this, arguments); + } + + return feedEvent; + }() + /** + * Feeds a to-device event to the widget. If the widget is not able to accept the + * event due to permissions, this will no-op and return calmly. If the widget failed + * to handle the event, this will raise an error. + * @param {IRoomEvent} rawEvent The event to (try to) send to the widget. + * @param {boolean} encrypted Whether the event contents were encrypted. + * @returns {Promise} Resolves when complete, rejects if there was an error sending. + */ + + }, { + key: "feedToDevice", + value: function () { + var _feedToDevice = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee7(rawEvent, encrypted) { + return _regeneratorRuntime().wrap(function _callee7$(_context7) { + while (1) { + switch (_context7.prev = _context7.next) { + case 0: + if (!this.canReceiveToDeviceEvent(rawEvent.type)) { + _context7.next = 3; + break; + } + + _context7.next = 3; + return this.transport.send(_WidgetApiAction.WidgetApiToWidgetAction.SendToDevice, // it's compatible, but missing the index signature + _objectSpread(_objectSpread({}, rawEvent), {}, { + encrypted: encrypted + })); + + case 3: + case "end": + return _context7.stop(); + } + } + }, _callee7, this); + })); + + function feedToDevice(_x9, _x10) { + return _feedToDevice.apply(this, arguments); + } + + return feedToDevice; + }() + }]); + + return ClientWidgetApi; +}(_events.EventEmitter); + +exports.ClientWidgetApi = ClientWidgetApi; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/driver/WidgetDriver.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/driver/WidgetDriver.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/driver/WidgetDriver.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/driver/WidgetDriver.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,214 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.WidgetDriver = void 0; + +var _ = require(".."); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } + +/** + * Represents the functions and behaviour the widget-api is unable to + * do, such as prompting the user for information or interacting with + * the UI. Clients are expected to implement this class and override + * any functions they need/want to support. + * + * This class assumes the client will have a context of a Widget + * instance already. + */ +var WidgetDriver = /*#__PURE__*/function () { + function WidgetDriver() { + _classCallCheck(this, WidgetDriver); + } + + _createClass(WidgetDriver, [{ + key: "validateCapabilities", + value: + /** + * Verifies the widget's requested capabilities, returning the ones + * it is approved to use. Mutating the requested capabilities will + * have no effect. + * + * This SHOULD result in the user being prompted to approve/deny + * capabilities. + * + * By default this rejects all capabilities (returns an empty set). + * @param {Set} requested The set of requested capabilities. + * @returns {Promise>} Resolves to the allowed capabilities. + */ + function validateCapabilities(requested) { + return Promise.resolve(new Set()); + } + /** + * Sends an event into a room. If `roomId` is falsy, the client should send the event + * into the room the user is currently looking at. The widget API will have already + * verified that the widget is capable of sending the event to that room. + * @param {string} eventType The event type to be sent. + * @param {*} content The content for the event. + * @param {string|null} stateKey The state key if this is a state event, otherwise null. + * May be an empty string. + * @param {string|null} roomId The room ID to send the event to. If falsy, the room the + * user is currently looking at. + * @returns {Promise} Resolves when the event has been sent with + * details of that event. + * @throws Rejected when the event could not be sent. + */ + + }, { + key: "sendEvent", + value: function sendEvent(eventType, content) { + var stateKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + var roomId = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + return Promise.reject(new Error("Failed to override function")); + } + /** + * Sends a to-device event. The widget API will have already verified that the widget + * is capable of sending the event. + * @param {string} eventType The event type to be sent. + * @param {boolean} encrypted Whether to encrypt the message contents. + * @param {Object} contentMap A map from user ID and device ID to event content. + * @returns {Promise} Resolves when the event has been sent. + * @throws Rejected when the event could not be sent. + */ + + }, { + key: "sendToDevice", + value: function sendToDevice(eventType, encrypted, contentMap) { + return Promise.reject(new Error("Failed to override function")); + } + /** + * Reads all events of the given type, and optionally `msgtype` (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that + * `limit` in each of the client's known rooms should be returned. When `null`, only the + * room the user is currently looking at should be considered. + * @param eventType The event type to be read. + * @param msgtype The msgtype of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve per room. Will be zero to denote "as many + * as possible". + * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs + * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. + * @returns {Promise} Resolves to the room events, or an empty array. + */ + + }, { + key: "readRoomEvents", + value: function readRoomEvents(eventType, msgtype, limit) { + var roomIds = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + return Promise.resolve([]); + } + /** + * Reads all events of the given type, and optionally state key (if applicable/defined), + * the user has access to. The widget API will have already verified that the widget is + * capable of receiving the events. Less events than the limit are allowed to be returned, + * but not more. If `roomIds` is supplied, it may contain `Symbols.AnyRoom` to denote that + * `limit` in each of the client's known rooms should be returned. When `null`, only the + * room the user is currently looking at should be considered. + * @param eventType The event type to be read. + * @param stateKey The state key of the events to be read, if applicable/defined. + * @param limit The maximum number of events to retrieve. Will be zero to denote "as many + * as possible". + * @param roomIds When null, the user's currently viewed room. Otherwise, the list of room IDs + * to look within, possibly containing Symbols.AnyRoom to denote all known rooms. + * @returns {Promise} Resolves to the state events, or an empty array. + */ + + }, { + key: "readStateEvents", + value: function readStateEvents(eventType, stateKey, limit) { + var roomIds = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + return Promise.resolve([]); + } + /** + * Reads all events that are related to a given event. The widget API will + * have already verified that the widget is capable of receiving the event, + * or will make sure to reject access to events which are returned from this + * function, but are not capable of receiving. If `relationType` or `eventType` + * are set, the returned events should already be filtered. Less events than + * the limit are allowed to be returned, but not more. + * @param eventId The id of the parent event to be read. + * @param roomId The room to look within. When undefined, the user's + * currently viewed room. + * @param relationType The relationship type of child events to search for. + * When undefined, all relations are returned. + * @param eventType The event type of child events to search for. When undefined, + * all related events are returned. + * @param from The pagination token to start returning results from, as + * received from a previous call. If not supplied, results start at the most + * recent topological event known to the server. + * @param to The pagination token to stop returning results at. If not + * supplied, results continue up to limit or until there are no more events. + * @param limit The maximum number of events to retrieve per room. If not + * supplied, the server will apply a default limit. + * @param direction The direction to search for according to MSC3715 + * @returns Resolves to the room relations. + */ + + }, { + key: "readEventRelations", + value: function readEventRelations(eventId, roomId, relationType, eventType, from, to, limit, direction) { + return Promise.resolve({ + chunk: [] + }); + } + /** + * Asks the user for permission to validate their identity through OpenID Connect. The + * interface for this function is an observable which accepts the state machine of the + * OIDC exchange flow. For example, if the client/user blocks the request then it would + * feed back a `{state: Blocked}` into the observable. Similarly, if the user already + * approved the widget then a `{state: Allowed}` would be fed into the observable alongside + * the token itself. If the client is asking for permission, it should feed in a + * `{state: PendingUserConfirmation}` followed by the relevant Allowed or Blocked state. + * + * The widget API will reject the widget's request with an error if this contract is not + * met properly. By default, the widget driver will block all OIDC requests. + * @param {SimpleObservable} observer The observable to feed updates into. + */ + + }, { + key: "askOpenID", + value: function askOpenID(observer) { + observer.update({ + state: _.OpenIDRequestState.Blocked + }); + } + /** + * Navigates the client with a matrix.to URI. In future this function will also be provided + * with the Matrix URIs once matrix.to is replaced. The given URI will have already been + * lightly checked to ensure it looks like a valid URI, though the implementation is recommended + * to do further checks on the URI. + * @param {string} uri The URI to navigate to. + * @returns {Promise} Resolves when complete. + * @throws Throws if there's a problem with the navigation, such as invalid format. + */ + + }, { + key: "navigate", + value: function navigate(uri) { + throw new Error("Navigation is not implemented"); + } + /** + * Polls for TURN server data, yielding an initial set of credentials as soon as possible, and + * thereafter yielding new credentials whenever the previous ones expire. The widget API will + * have already verified that the widget has permission to access TURN servers. + * @yields {ITurnServer} The TURN server URIs and credentials currently available to the client. + */ + + }, { + key: "getTurnServers", + value: function getTurnServers() { + throw new Error("TURN server support is not implemented"); + } + }]); + + return WidgetDriver; +}(); + +exports.WidgetDriver = WidgetDriver; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/index.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,603 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _WidgetApi = require("./WidgetApi"); + +Object.keys(_WidgetApi).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _WidgetApi[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _WidgetApi[key]; + } + }); +}); + +var _ClientWidgetApi = require("./ClientWidgetApi"); + +Object.keys(_ClientWidgetApi).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ClientWidgetApi[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ClientWidgetApi[key]; + } + }); +}); + +var _Symbols = require("./Symbols"); + +Object.keys(_Symbols).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _Symbols[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _Symbols[key]; + } + }); +}); + +var _ITransport = require("./transport/ITransport"); + +Object.keys(_ITransport).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ITransport[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ITransport[key]; + } + }); +}); + +var _PostmessageTransport = require("./transport/PostmessageTransport"); + +Object.keys(_PostmessageTransport).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _PostmessageTransport[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _PostmessageTransport[key]; + } + }); +}); + +var _ICustomWidgetData = require("./interfaces/ICustomWidgetData"); + +Object.keys(_ICustomWidgetData).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ICustomWidgetData[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ICustomWidgetData[key]; + } + }); +}); + +var _IJitsiWidgetData = require("./interfaces/IJitsiWidgetData"); + +Object.keys(_IJitsiWidgetData).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _IJitsiWidgetData[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _IJitsiWidgetData[key]; + } + }); +}); + +var _IStickerpickerWidgetData = require("./interfaces/IStickerpickerWidgetData"); + +Object.keys(_IStickerpickerWidgetData).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _IStickerpickerWidgetData[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _IStickerpickerWidgetData[key]; + } + }); +}); + +var _IWidget = require("./interfaces/IWidget"); + +Object.keys(_IWidget).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _IWidget[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _IWidget[key]; + } + }); +}); + +var _WidgetType = require("./interfaces/WidgetType"); + +Object.keys(_WidgetType).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _WidgetType[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _WidgetType[key]; + } + }); +}); + +var _IWidgetApiErrorResponse = require("./interfaces/IWidgetApiErrorResponse"); + +Object.keys(_IWidgetApiErrorResponse).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _IWidgetApiErrorResponse[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _IWidgetApiErrorResponse[key]; + } + }); +}); + +var _IWidgetApiRequest = require("./interfaces/IWidgetApiRequest"); + +Object.keys(_IWidgetApiRequest).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _IWidgetApiRequest[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _IWidgetApiRequest[key]; + } + }); +}); + +var _IWidgetApiResponse = require("./interfaces/IWidgetApiResponse"); + +Object.keys(_IWidgetApiResponse).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _IWidgetApiResponse[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _IWidgetApiResponse[key]; + } + }); +}); + +var _WidgetApiAction = require("./interfaces/WidgetApiAction"); + +Object.keys(_WidgetApiAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _WidgetApiAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _WidgetApiAction[key]; + } + }); +}); + +var _WidgetApiDirection = require("./interfaces/WidgetApiDirection"); + +Object.keys(_WidgetApiDirection).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _WidgetApiDirection[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _WidgetApiDirection[key]; + } + }); +}); + +var _ApiVersion = require("./interfaces/ApiVersion"); + +Object.keys(_ApiVersion).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ApiVersion[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ApiVersion[key]; + } + }); +}); + +var _Capabilities = require("./interfaces/Capabilities"); + +Object.keys(_Capabilities).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _Capabilities[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _Capabilities[key]; + } + }); +}); + +var _CapabilitiesAction = require("./interfaces/CapabilitiesAction"); + +Object.keys(_CapabilitiesAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _CapabilitiesAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _CapabilitiesAction[key]; + } + }); +}); + +var _ContentLoadedAction = require("./interfaces/ContentLoadedAction"); + +Object.keys(_ContentLoadedAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ContentLoadedAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ContentLoadedAction[key]; + } + }); +}); + +var _ScreenshotAction = require("./interfaces/ScreenshotAction"); + +Object.keys(_ScreenshotAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ScreenshotAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ScreenshotAction[key]; + } + }); +}); + +var _StickerAction = require("./interfaces/StickerAction"); + +Object.keys(_StickerAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _StickerAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _StickerAction[key]; + } + }); +}); + +var _StickyAction = require("./interfaces/StickyAction"); + +Object.keys(_StickyAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _StickyAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _StickyAction[key]; + } + }); +}); + +var _SupportedVersionsAction = require("./interfaces/SupportedVersionsAction"); + +Object.keys(_SupportedVersionsAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _SupportedVersionsAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _SupportedVersionsAction[key]; + } + }); +}); + +var _VisibilityAction = require("./interfaces/VisibilityAction"); + +Object.keys(_VisibilityAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _VisibilityAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _VisibilityAction[key]; + } + }); +}); + +var _GetOpenIDAction = require("./interfaces/GetOpenIDAction"); + +Object.keys(_GetOpenIDAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _GetOpenIDAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _GetOpenIDAction[key]; + } + }); +}); + +var _OpenIDCredentialsAction = require("./interfaces/OpenIDCredentialsAction"); + +Object.keys(_OpenIDCredentialsAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _OpenIDCredentialsAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _OpenIDCredentialsAction[key]; + } + }); +}); + +var _WidgetKind = require("./interfaces/WidgetKind"); + +Object.keys(_WidgetKind).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _WidgetKind[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _WidgetKind[key]; + } + }); +}); + +var _ModalButtonKind = require("./interfaces/ModalButtonKind"); + +Object.keys(_ModalButtonKind).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ModalButtonKind[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ModalButtonKind[key]; + } + }); +}); + +var _ModalWidgetActions = require("./interfaces/ModalWidgetActions"); + +Object.keys(_ModalWidgetActions).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ModalWidgetActions[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ModalWidgetActions[key]; + } + }); +}); + +var _SetModalButtonEnabledAction = require("./interfaces/SetModalButtonEnabledAction"); + +Object.keys(_SetModalButtonEnabledAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _SetModalButtonEnabledAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _SetModalButtonEnabledAction[key]; + } + }); +}); + +var _WidgetConfigAction = require("./interfaces/WidgetConfigAction"); + +Object.keys(_WidgetConfigAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _WidgetConfigAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _WidgetConfigAction[key]; + } + }); +}); + +var _SendEventAction = require("./interfaces/SendEventAction"); + +Object.keys(_SendEventAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _SendEventAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _SendEventAction[key]; + } + }); +}); + +var _SendToDeviceAction = require("./interfaces/SendToDeviceAction"); + +Object.keys(_SendToDeviceAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _SendToDeviceAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _SendToDeviceAction[key]; + } + }); +}); + +var _ReadEventAction = require("./interfaces/ReadEventAction"); + +Object.keys(_ReadEventAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ReadEventAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ReadEventAction[key]; + } + }); +}); + +var _IRoomEvent = require("./interfaces/IRoomEvent"); + +Object.keys(_IRoomEvent).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _IRoomEvent[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _IRoomEvent[key]; + } + }); +}); + +var _NavigateAction = require("./interfaces/NavigateAction"); + +Object.keys(_NavigateAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _NavigateAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _NavigateAction[key]; + } + }); +}); + +var _TurnServerActions = require("./interfaces/TurnServerActions"); + +Object.keys(_TurnServerActions).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _TurnServerActions[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _TurnServerActions[key]; + } + }); +}); + +var _ReadRelationsAction = require("./interfaces/ReadRelationsAction"); + +Object.keys(_ReadRelationsAction).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _ReadRelationsAction[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _ReadRelationsAction[key]; + } + }); +}); + +var _WidgetEventCapability = require("./models/WidgetEventCapability"); + +Object.keys(_WidgetEventCapability).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _WidgetEventCapability[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _WidgetEventCapability[key]; + } + }); +}); + +var _url = require("./models/validation/url"); + +Object.keys(_url).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _url[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _url[key]; + } + }); +}); + +var _utils = require("./models/validation/utils"); + +Object.keys(_utils).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _utils[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _utils[key]; + } + }); +}); + +var _Widget = require("./models/Widget"); + +Object.keys(_Widget).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _Widget[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _Widget[key]; + } + }); +}); + +var _WidgetParser = require("./models/WidgetParser"); + +Object.keys(_WidgetParser).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _WidgetParser[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _WidgetParser[key]; + } + }); +}); + +var _urlTemplate = require("./templating/url-template"); + +Object.keys(_urlTemplate).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _urlTemplate[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _urlTemplate[key]; + } + }); +}); + +var _SimpleObservable = require("./util/SimpleObservable"); + +Object.keys(_SimpleObservable).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _SimpleObservable[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _SimpleObservable[key]; + } + }); +}); + +var _WidgetDriver = require("./driver/WidgetDriver"); + +Object.keys(_WidgetDriver).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _WidgetDriver[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _WidgetDriver[key]; + } + }); +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ApiVersion.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ApiVersion.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ApiVersion.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ApiVersion.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,47 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.UnstableApiVersion = exports.MatrixApiVersion = exports.CurrentApiVersions = void 0; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var MatrixApiVersion; +exports.MatrixApiVersion = MatrixApiVersion; + +(function (MatrixApiVersion) { + MatrixApiVersion["Prerelease1"] = "0.0.1"; + MatrixApiVersion["Prerelease2"] = "0.0.2"; +})(MatrixApiVersion || (exports.MatrixApiVersion = MatrixApiVersion = {})); + +var UnstableApiVersion; +exports.UnstableApiVersion = UnstableApiVersion; + +(function (UnstableApiVersion) { + UnstableApiVersion["MSC2762"] = "org.matrix.msc2762"; + UnstableApiVersion["MSC2871"] = "org.matrix.msc2871"; + UnstableApiVersion["MSC2931"] = "org.matrix.msc2931"; + UnstableApiVersion["MSC2974"] = "org.matrix.msc2974"; + UnstableApiVersion["MSC2876"] = "org.matrix.msc2876"; + UnstableApiVersion["MSC3819"] = "org.matrix.msc3819"; + UnstableApiVersion["MSC3846"] = "town.robin.msc3846"; + UnstableApiVersion["MSC3869"] = "org.matrix.msc3869"; +})(UnstableApiVersion || (exports.UnstableApiVersion = UnstableApiVersion = {})); + +var CurrentApiVersions = [MatrixApiVersion.Prerelease1, MatrixApiVersion.Prerelease2, //MatrixApiVersion.V010, +UnstableApiVersion.MSC2762, UnstableApiVersion.MSC2871, UnstableApiVersion.MSC2931, UnstableApiVersion.MSC2974, UnstableApiVersion.MSC2876, UnstableApiVersion.MSC3819, UnstableApiVersion.MSC3846, UnstableApiVersion.MSC3869]; +exports.CurrentApiVersions = CurrentApiVersions; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/CapabilitiesAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/CapabilitiesAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/CapabilitiesAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/CapabilitiesAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/Capabilities.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/Capabilities.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/Capabilities.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/Capabilities.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,73 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.VideoConferenceCapabilities = exports.StickerpickerCapabilities = exports.MatrixCapabilities = void 0; +exports.getTimelineRoomIDFromCapability = getTimelineRoomIDFromCapability; +exports.isTimelineCapability = isTimelineCapability; +exports.isTimelineCapabilityFor = isTimelineCapabilityFor; + +/* + * Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var MatrixCapabilities; +exports.MatrixCapabilities = MatrixCapabilities; + +(function (MatrixCapabilities) { + MatrixCapabilities["Screenshots"] = "m.capability.screenshot"; + MatrixCapabilities["StickerSending"] = "m.sticker"; + MatrixCapabilities["AlwaysOnScreen"] = "m.always_on_screen"; + MatrixCapabilities["RequiresClient"] = "io.element.requires_client"; + MatrixCapabilities["MSC2931Navigate"] = "org.matrix.msc2931.navigate"; + MatrixCapabilities["MSC3846TurnServers"] = "town.robin.msc3846.turn_servers"; +})(MatrixCapabilities || (exports.MatrixCapabilities = MatrixCapabilities = {})); + +var StickerpickerCapabilities = [MatrixCapabilities.StickerSending]; +exports.StickerpickerCapabilities = StickerpickerCapabilities; +var VideoConferenceCapabilities = [MatrixCapabilities.AlwaysOnScreen]; +/** + * Determines if a capability is a capability for a timeline. + * @param {Capability} capability The capability to test. + * @returns {boolean} True if a timeline capability, false otherwise. + */ + +exports.VideoConferenceCapabilities = VideoConferenceCapabilities; + +function isTimelineCapability(capability) { + // TODO: Change when MSC2762 becomes stable. + return capability === null || capability === void 0 ? void 0 : capability.startsWith("org.matrix.msc2762.timeline:"); +} +/** + * Determines if a capability is a timeline capability for the given room. + * @param {Capability} capability The capability to test. + * @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` for that designation. + * @returns {boolean} True if a matching capability, false otherwise. + */ + + +function isTimelineCapabilityFor(capability, roomId) { + return capability === "org.matrix.msc2762.timeline:".concat(roomId); +} +/** + * Gets the room ID described by a timeline capability. + * @param {string} capability The capability to parse. + * @returns {string} The room ID. + */ + + +function getTimelineRoomIDFromCapability(capability) { + return capability.substring(capability.indexOf(":") + 1); +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ContentLoadedAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ContentLoadedAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ContentLoadedAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ContentLoadedAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/GetOpenIDAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/GetOpenIDAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/GetOpenIDAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/GetOpenIDAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,30 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.OpenIDRequestState = void 0; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var OpenIDRequestState; +exports.OpenIDRequestState = OpenIDRequestState; + +(function (OpenIDRequestState) { + OpenIDRequestState["Allowed"] = "allowed"; + OpenIDRequestState["Blocked"] = "blocked"; + OpenIDRequestState["PendingUserConfirmation"] = "request"; +})(OpenIDRequestState || (exports.OpenIDRequestState = OpenIDRequestState = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ICustomWidgetData.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ICustomWidgetData.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ICustomWidgetData.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ICustomWidgetData.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IJitsiWidgetData.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IJitsiWidgetData.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IJitsiWidgetData.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IJitsiWidgetData.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IRoomEvent.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IRoomEvent.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IRoomEvent.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IRoomEvent.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IStickerpickerWidgetData.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IStickerpickerWidgetData.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IStickerpickerWidgetData.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IStickerpickerWidgetData.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiErrorResponse.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiErrorResponse.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiErrorResponse.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiErrorResponse.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,30 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.isErrorResponse = isErrorResponse; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +function isErrorResponse(responseData) { + if ("error" in responseData) { + var err = responseData; + return !!err.error.message; + } + + return false; +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiRequest.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiRequest.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiRequest.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiRequest.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiResponse.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiResponse.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiResponse.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidgetApiResponse.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidget.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidget.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidget.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/IWidget.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ModalButtonKind.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ModalButtonKind.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ModalButtonKind.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ModalButtonKind.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,32 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ModalButtonKind = void 0; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var ModalButtonKind; +exports.ModalButtonKind = ModalButtonKind; + +(function (ModalButtonKind) { + ModalButtonKind["Primary"] = "m.primary"; + ModalButtonKind["Secondary"] = "m.secondary"; + ModalButtonKind["Warning"] = "m.warning"; + ModalButtonKind["Danger"] = "m.danger"; + ModalButtonKind["Link"] = "m.link"; +})(ModalButtonKind || (exports.ModalButtonKind = ModalButtonKind = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ModalWidgetActions.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ModalWidgetActions.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ModalWidgetActions.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ModalWidgetActions.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,28 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.BuiltInModalButtonID = void 0; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var BuiltInModalButtonID; +exports.BuiltInModalButtonID = BuiltInModalButtonID; + +(function (BuiltInModalButtonID) { + BuiltInModalButtonID["Close"] = "m.close"; +})(BuiltInModalButtonID || (exports.BuiltInModalButtonID = BuiltInModalButtonID = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/NavigateAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/NavigateAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/NavigateAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/NavigateAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/OpenIDCredentialsAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/OpenIDCredentialsAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/OpenIDCredentialsAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/OpenIDCredentialsAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ReadEventAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ReadEventAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ReadEventAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ReadEventAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ReadRelationsAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ReadRelationsAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ReadRelationsAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ReadRelationsAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ScreenshotAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ScreenshotAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ScreenshotAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/ScreenshotAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SendEventAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SendEventAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SendEventAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SendEventAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SendToDeviceAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SendToDeviceAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SendToDeviceAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SendToDeviceAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SetModalButtonEnabledAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SetModalButtonEnabledAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SetModalButtonEnabledAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SetModalButtonEnabledAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/StickerAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/StickerAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/StickerAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/StickerAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/StickyAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/StickyAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/StickyAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/StickyAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SupportedVersionsAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SupportedVersionsAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SupportedVersionsAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/SupportedVersionsAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/TurnServerActions.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/TurnServerActions.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/TurnServerActions.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/TurnServerActions.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/VisibilityAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/VisibilityAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/VisibilityAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/VisibilityAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetApiAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetApiAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetApiAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetApiAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,61 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.WidgetApiToWidgetAction = exports.WidgetApiFromWidgetAction = void 0; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var WidgetApiToWidgetAction; +exports.WidgetApiToWidgetAction = WidgetApiToWidgetAction; + +(function (WidgetApiToWidgetAction) { + WidgetApiToWidgetAction["SupportedApiVersions"] = "supported_api_versions"; + WidgetApiToWidgetAction["Capabilities"] = "capabilities"; + WidgetApiToWidgetAction["NotifyCapabilities"] = "notify_capabilities"; + WidgetApiToWidgetAction["TakeScreenshot"] = "screenshot"; + WidgetApiToWidgetAction["UpdateVisibility"] = "visibility"; + WidgetApiToWidgetAction["OpenIDCredentials"] = "openid_credentials"; + WidgetApiToWidgetAction["WidgetConfig"] = "widget_config"; + WidgetApiToWidgetAction["CloseModalWidget"] = "close_modal"; + WidgetApiToWidgetAction["ButtonClicked"] = "button_clicked"; + WidgetApiToWidgetAction["SendEvent"] = "send_event"; + WidgetApiToWidgetAction["SendToDevice"] = "send_to_device"; + WidgetApiToWidgetAction["UpdateTurnServers"] = "update_turn_servers"; +})(WidgetApiToWidgetAction || (exports.WidgetApiToWidgetAction = WidgetApiToWidgetAction = {})); + +var WidgetApiFromWidgetAction; +exports.WidgetApiFromWidgetAction = WidgetApiFromWidgetAction; + +(function (WidgetApiFromWidgetAction) { + WidgetApiFromWidgetAction["SupportedApiVersions"] = "supported_api_versions"; + WidgetApiFromWidgetAction["ContentLoaded"] = "content_loaded"; + WidgetApiFromWidgetAction["SendSticker"] = "m.sticker"; + WidgetApiFromWidgetAction["UpdateAlwaysOnScreen"] = "set_always_on_screen"; + WidgetApiFromWidgetAction["GetOpenIDCredentials"] = "get_openid"; + WidgetApiFromWidgetAction["CloseModalWidget"] = "close_modal"; + WidgetApiFromWidgetAction["OpenModalWidget"] = "open_modal"; + WidgetApiFromWidgetAction["SetModalButtonEnabled"] = "set_button_enabled"; + WidgetApiFromWidgetAction["SendEvent"] = "send_event"; + WidgetApiFromWidgetAction["SendToDevice"] = "send_to_device"; + WidgetApiFromWidgetAction["WatchTurnServers"] = "watch_turn_servers"; + WidgetApiFromWidgetAction["UnwatchTurnServers"] = "unwatch_turn_servers"; + WidgetApiFromWidgetAction["MSC2876ReadEvents"] = "org.matrix.msc2876.read_events"; + WidgetApiFromWidgetAction["MSC2931Navigate"] = "org.matrix.msc2931.navigate"; + WidgetApiFromWidgetAction["MSC2974RenegotiateCapabilities"] = "org.matrix.msc2974.request_capabilities"; + WidgetApiFromWidgetAction["MSC3869ReadRelations"] = "org.matrix.msc3869.read_relations"; +})(WidgetApiFromWidgetAction || (exports.WidgetApiFromWidgetAction = WidgetApiFromWidgetAction = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetApiDirection.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetApiDirection.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetApiDirection.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetApiDirection.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,40 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.WidgetApiDirection = void 0; +exports.invertedDirection = invertedDirection; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var WidgetApiDirection; +exports.WidgetApiDirection = WidgetApiDirection; + +(function (WidgetApiDirection) { + WidgetApiDirection["ToWidget"] = "toWidget"; + WidgetApiDirection["FromWidget"] = "fromWidget"; +})(WidgetApiDirection || (exports.WidgetApiDirection = WidgetApiDirection = {})); + +function invertedDirection(dir) { + if (dir === WidgetApiDirection.ToWidget) { + return WidgetApiDirection.FromWidget; + } else if (dir === WidgetApiDirection.FromWidget) { + return WidgetApiDirection.ToWidget; + } else { + throw new Error("Invalid direction"); + } +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetConfigAction.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetConfigAction.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetConfigAction.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetConfigAction.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetKind.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetKind.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetKind.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetKind.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,30 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.WidgetKind = void 0; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var WidgetKind; +exports.WidgetKind = WidgetKind; + +(function (WidgetKind) { + WidgetKind["Room"] = "room"; + WidgetKind["Account"] = "account"; + WidgetKind["Modal"] = "modal"; +})(WidgetKind || (exports.WidgetKind = WidgetKind = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetType.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetType.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetType.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/interfaces/WidgetType.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,30 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MatrixWidgetType = void 0; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var MatrixWidgetType; +exports.MatrixWidgetType = MatrixWidgetType; + +(function (MatrixWidgetType) { + MatrixWidgetType["Custom"] = "m.custom"; + MatrixWidgetType["JitsiMeet"] = "m.jitsi"; + MatrixWidgetType["Stickerpicker"] = "m.stickerpicker"; +})(MatrixWidgetType || (exports.MatrixWidgetType = MatrixWidgetType = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/LICENSE thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/LICENSE --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/LICENSE 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/validation/url.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/validation/url.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/validation/url.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/validation/url.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,41 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.isValidUrl = isValidUrl; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +function isValidUrl(val) { + if (!val) return false; // easy: not valid if not present + + try { + var parsed = new URL(val); + + if (parsed.protocol !== "http" && parsed.protocol !== "https") { + return false; + } + + return true; + } catch (e) { + if (e instanceof TypeError) { + return false; + } + + throw e; + } +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/validation/utils.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/validation/utils.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/validation/utils.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/validation/utils.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,27 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.assertPresent = assertPresent; + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +function assertPresent(obj, key) { + if (!obj[key]) { + throw new Error("".concat(key, " is required")); + } +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/WidgetEventCapability.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/WidgetEventCapability.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/WidgetEventCapability.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/WidgetEventCapability.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,253 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.WidgetEventCapability = exports.EventKind = exports.EventDirection = void 0; + +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var EventKind; +exports.EventKind = EventKind; + +(function (EventKind) { + EventKind["Event"] = "event"; + EventKind["State"] = "state_event"; + EventKind["ToDevice"] = "to_device"; +})(EventKind || (exports.EventKind = EventKind = {})); + +var EventDirection; +exports.EventDirection = EventDirection; + +(function (EventDirection) { + EventDirection["Send"] = "send"; + EventDirection["Receive"] = "receive"; +})(EventDirection || (exports.EventDirection = EventDirection = {})); + +var WidgetEventCapability = /*#__PURE__*/function () { + function WidgetEventCapability(direction, eventType, kind, keyStr, raw) { + _classCallCheck(this, WidgetEventCapability); + + this.direction = direction; + this.eventType = eventType; + this.kind = kind; + this.keyStr = keyStr; + this.raw = raw; + } + + _createClass(WidgetEventCapability, [{ + key: "matchesAsStateEvent", + value: function matchesAsStateEvent(direction, eventType, stateKey) { + if (this.kind !== EventKind.State) return false; // not a state event + + if (this.direction !== direction) return false; // direction mismatch + + if (this.eventType !== eventType) return false; // event type mismatch + + if (this.keyStr === null) return true; // all state keys are allowed + + if (this.keyStr === stateKey) return true; // this state key is allowed + // Default not allowed + + return false; + } + }, { + key: "matchesAsToDeviceEvent", + value: function matchesAsToDeviceEvent(direction, eventType) { + if (this.kind !== EventKind.ToDevice) return false; // not a to-device event + + if (this.direction !== direction) return false; // direction mismatch + + if (this.eventType !== eventType) return false; // event type mismatch + // Checks passed, the event is allowed + + return true; + } + }, { + key: "matchesAsRoomEvent", + value: function matchesAsRoomEvent(direction, eventType) { + var msgtype = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + if (this.kind !== EventKind.Event) return false; // not a room event + + if (this.direction !== direction) return false; // direction mismatch + + if (this.eventType !== eventType) return false; // event type mismatch + + if (this.eventType === "m.room.message") { + if (this.keyStr === null) return true; // all message types are allowed + + if (this.keyStr === msgtype) return true; // this message type is allowed + } else { + return true; // already passed the check for if the event is allowed + } // Default not allowed + + + return false; + } + }], [{ + key: "forStateEvent", + value: function forStateEvent(direction, eventType, stateKey) { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + eventType = eventType.replace(/#/g, '\\#'); + stateKey = stateKey !== null && stateKey !== undefined ? "#".concat(stateKey) : ''; + var str = "org.matrix.msc2762.".concat(direction, ".state_event:").concat(eventType).concat(stateKey); // cheat by sending it through the processor + + return WidgetEventCapability.findEventCapabilities([str])[0]; + } + }, { + key: "forToDeviceEvent", + value: function forToDeviceEvent(direction, eventType) { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/56 + var str = "org.matrix.msc3819.".concat(direction, ".to_device:").concat(eventType); // cheat by sending it through the processor + + return WidgetEventCapability.findEventCapabilities([str])[0]; + } + }, { + key: "forRoomEvent", + value: function forRoomEvent(direction, eventType) { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + var str = "org.matrix.msc2762.".concat(direction, ".event:").concat(eventType); // cheat by sending it through the processor + + return WidgetEventCapability.findEventCapabilities([str])[0]; + } + }, { + key: "forRoomMessageEvent", + value: function forRoomMessageEvent(direction, msgtype) { + // TODO: Enable support for m.* namespace once the MSC lands. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + msgtype = msgtype === null || msgtype === undefined ? '' : msgtype; + var str = "org.matrix.msc2762.".concat(direction, ".event:m.room.message#").concat(msgtype); // cheat by sending it through the processor + + return WidgetEventCapability.findEventCapabilities([str])[0]; + } + /** + * Parses a capabilities request to find all the event capability requests. + * @param {Iterable} capabilities The capabilities requested/to parse. + * @returns {WidgetEventCapability[]} An array of event capability requests. May be empty, but never null. + */ + + }, { + key: "findEventCapabilities", + value: function findEventCapabilities(capabilities) { + var parsed = []; + + var _iterator = _createForOfIteratorHelper(capabilities), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var cap = _step.value; + var _direction = null; + var eventSegment = void 0; + var _kind = null; // TODO: Enable support for m.* namespace once the MSCs land. + // https://github.com/matrix-org/matrix-widget-api/issues/22 + // https://github.com/matrix-org/matrix-widget-api/issues/56 + + if (cap.startsWith("org.matrix.msc2762.send.event:")) { + _direction = EventDirection.Send; + _kind = EventKind.Event; + eventSegment = cap.substring("org.matrix.msc2762.send.event:".length); + } else if (cap.startsWith("org.matrix.msc2762.send.state_event:")) { + _direction = EventDirection.Send; + _kind = EventKind.State; + eventSegment = cap.substring("org.matrix.msc2762.send.state_event:".length); + } else if (cap.startsWith("org.matrix.msc3819.send.to_device:")) { + _direction = EventDirection.Send; + _kind = EventKind.ToDevice; + eventSegment = cap.substring("org.matrix.msc3819.send.to_device:".length); + } else if (cap.startsWith("org.matrix.msc2762.receive.event:")) { + _direction = EventDirection.Receive; + _kind = EventKind.Event; + eventSegment = cap.substring("org.matrix.msc2762.receive.event:".length); + } else if (cap.startsWith("org.matrix.msc2762.receive.state_event:")) { + _direction = EventDirection.Receive; + _kind = EventKind.State; + eventSegment = cap.substring("org.matrix.msc2762.receive.state_event:".length); + } else if (cap.startsWith("org.matrix.msc3819.receive.to_device:")) { + _direction = EventDirection.Receive; + _kind = EventKind.ToDevice; + eventSegment = cap.substring("org.matrix.msc3819.receive.to_device:".length); + } + + if (_direction === null || _kind === null) continue; // The capability uses `#` as a separator between event type and state key/msgtype, + // so we split on that. However, a # is also valid in either one of those so we + // join accordingly. + // Eg: `m.room.message##m.text` is "m.room.message" event with msgtype "#m.text". + + var expectingKeyStr = eventSegment.startsWith("m.room.message#") || _kind === EventKind.State; + + var _keyStr = null; + + if (eventSegment.includes('#') && expectingKeyStr) { + // Dev note: regex is difficult to write, so instead the rules are manually written + // out. This is probably just as understandable as a boring regex though, so win-win? + // Test cases: + // str eventSegment keyStr + // ------------------------------------------------------------- + // m.room.message# m.room.message + // m.room.message#test m.room.message test + // m.room.message\# m.room.message# test + // m.room.message##test m.room.message #test + // m.room.message\##test m.room.message# test + // m.room.message\\##test m.room.message\# test + // m.room.message\\###test m.room.message\# #test + // First step: explode the string + var parts = eventSegment.split('#'); // To form the eventSegment, we'll keep finding parts of the exploded string until + // there's one that doesn't end with the escape character (\). We'll then join those + // segments together with the exploding character. We have to remember to consume the + // escape character as well. + + var idx = parts.findIndex(function (p) { + return !p.endsWith("\\"); + }); + eventSegment = parts.slice(0, idx + 1).map(function (p) { + return p.endsWith('\\') ? p.substring(0, p.length - 1) : p; + }).join('#'); // The keyStr is whatever is left over. + + _keyStr = parts.slice(idx + 1).join('#'); + } + + parsed.push(new WidgetEventCapability(_direction, eventSegment, _kind, _keyStr, cap)); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + return parsed; + } + }]); + + return WidgetEventCapability; +}(); + +exports.WidgetEventCapability = WidgetEventCapability; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/Widget.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/Widget.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/Widget.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/Widget.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,134 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Widget = void 0; + +var _utils = require("./validation/utils"); + +var _ = require(".."); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } + +/** + * Represents the barest form of widget. + */ +var Widget = /*#__PURE__*/function () { + function Widget(definition) { + _classCallCheck(this, Widget); + + this.definition = definition; + if (!this.definition) throw new Error("Definition is required"); + (0, _utils.assertPresent)(definition, "id"); + (0, _utils.assertPresent)(definition, "creatorUserId"); + (0, _utils.assertPresent)(definition, "type"); + (0, _utils.assertPresent)(definition, "url"); + } + /** + * The user ID who created the widget. + */ + + + _createClass(Widget, [{ + key: "creatorUserId", + get: function get() { + return this.definition.creatorUserId; + } + /** + * The type of widget. + */ + + }, { + key: "type", + get: function get() { + return this.definition.type; + } + /** + * The ID of the widget. + */ + + }, { + key: "id", + get: function get() { + return this.definition.id; + } + /** + * The name of the widget, or null if not set. + */ + + }, { + key: "name", + get: function get() { + return this.definition.name || null; + } + /** + * The title for the widget, or null if not set. + */ + + }, { + key: "title", + get: function get() { + return this.rawData.title || null; + } + /** + * The templated URL for the widget. + */ + + }, { + key: "templateUrl", + get: function get() { + return this.definition.url; + } + /** + * The origin for this widget. + */ + + }, { + key: "origin", + get: function get() { + return new URL(this.templateUrl).origin; + } + /** + * Whether or not the client should wait for the iframe to load. Defaults + * to true. + */ + + }, { + key: "waitForIframeLoad", + get: function get() { + if (this.definition.waitForIframeLoad === false) return false; + if (this.definition.waitForIframeLoad === true) return true; + return true; // default true + } + /** + * The raw data for the widget. This will always be defined, though + * may be empty. + */ + + }, { + key: "rawData", + get: function get() { + return this.definition.data || {}; + } + /** + * Gets a complete widget URL for the client to render. + * @param {ITemplateParams} params The template parameters. + * @returns {string} A templated URL. + */ + + }, { + key: "getCompleteUrl", + value: function getCompleteUrl(params) { + return (0, _.runTemplate)(this.templateUrl, this.definition, params); + } + }]); + + return Widget; +}(); + +exports.Widget = Widget; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/WidgetParser.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/WidgetParser.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/models/WidgetParser.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/models/WidgetParser.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,150 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.WidgetParser = void 0; + +var _Widget = require("./Widget"); + +var _url = require("./validation/url"); + +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } + +var WidgetParser = /*#__PURE__*/function () { + function WidgetParser() {// private constructor because this is a util class + + _classCallCheck(this, WidgetParser); + } + /** + * Parses widgets from the "m.widgets" account data event. This will always + * return an array, though may be empty if no valid widgets were found. + * @param {IAccountDataWidgets} content The content of the "m.widgets" account data. + * @returns {Widget[]} The widgets in account data, or an empty array. + */ + + + _createClass(WidgetParser, null, [{ + key: "parseAccountData", + value: function parseAccountData(content) { + if (!content) return []; + var result = []; + + for (var _i = 0, _Object$keys = Object.keys(content); _i < _Object$keys.length; _i++) { + var _widgetId = _Object$keys[_i]; + var roughWidget = content[_widgetId]; + if (!roughWidget) continue; + if (roughWidget.type !== "m.widget" && roughWidget.type !== "im.vector.modular.widgets") continue; + if (!roughWidget.sender) continue; + var probableWidgetId = roughWidget.state_key || roughWidget.id; + if (probableWidgetId !== _widgetId) continue; + var asStateEvent = { + content: roughWidget.content, + sender: roughWidget.sender, + type: "m.widget", + state_key: _widgetId, + event_id: "$example", + room_id: "!example", + origin_server_ts: 1 + }; + var widget = WidgetParser.parseRoomWidget(asStateEvent); + if (widget) result.push(widget); + } + + return result; + } + /** + * Parses all the widgets possible in the given array. This will always return + * an array, though may be empty if no widgets could be parsed. + * @param {IStateEvent[]} currentState The room state to parse. + * @returns {Widget[]} The widgets in the state, or an empty array. + */ + + }, { + key: "parseWidgetsFromRoomState", + value: function parseWidgetsFromRoomState(currentState) { + if (!currentState) return []; + var result = []; + + var _iterator = _createForOfIteratorHelper(currentState), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var state = _step.value; + var widget = WidgetParser.parseRoomWidget(state); + if (widget) result.push(widget); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + return result; + } + /** + * Parses a state event into a widget. If the state event does not represent + * a widget (wrong event type, invalid widget, etc) then null is returned. + * @param {IStateEvent} stateEvent The state event. + * @returns {Widget|null} The widget, or null if invalid + */ + + }, { + key: "parseRoomWidget", + value: function parseRoomWidget(stateEvent) { + if (!stateEvent) return null; // TODO: [Legacy] Remove legacy support + + if (stateEvent.type !== "m.widget" && stateEvent.type !== "im.vector.modular.widgets") { + return null; + } // Dev note: Throughout this function we have null safety to ensure that + // if the caller did not supply something useful that we don't error. This + // is done against the requirements of the interface because not everyone + // will have an interface to validate against. + + + var content = stateEvent.content || {}; // Form our best approximation of a widget with the information we have + + var estimatedWidget = { + id: stateEvent.state_key, + creatorUserId: content['creatorUserId'] || stateEvent.sender, + name: content['name'], + type: content['type'], + url: content['url'], + waitForIframeLoad: content['waitForIframeLoad'], + data: content['data'] + }; // Finally, process that widget + + return WidgetParser.processEstimatedWidget(estimatedWidget); + } + }, { + key: "processEstimatedWidget", + value: function processEstimatedWidget(widget) { + // Validate that the widget has the best chance of passing as a widget + if (!widget.id || !widget.creatorUserId || !widget.type) { + return null; + } + + if (!(0, _url.isValidUrl)(widget.url)) { + return null; + } // TODO: Validate data for known widget types + + + return new _Widget.Widget(widget); + } + }]); + + return WidgetParser; +}(); + +exports.WidgetParser = WidgetParser; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/Symbols.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/Symbols.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/Symbols.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/Symbols.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,28 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Symbols = void 0; + +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var Symbols; +exports.Symbols = Symbols; + +(function (Symbols) { + Symbols["AnyRoom"] = "*"; +})(Symbols || (exports.Symbols = Symbols = {})); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/templating/url-template.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/templating/url-template.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/templating/url-template.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/templating/url-template.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,60 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.runTemplate = runTemplate; +exports.toString = toString; + +/* + * Copyright 2020, 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +function runTemplate(url, widget, params) { + // Always apply the supplied params over top of data to ensure the data can't lie about them. + var variables = Object.assign({}, widget.data, { + 'matrix_room_id': params.widgetRoomId || "", + 'matrix_user_id': params.currentUserId, + 'matrix_display_name': params.userDisplayName || params.currentUserId, + 'matrix_avatar_url': params.userHttpAvatarUrl || "", + 'matrix_widget_id': widget.id, + // TODO: Convert to stable (https://github.com/matrix-org/matrix-doc/pull/2873) + 'org.matrix.msc2873.client_id': params.clientId || "", + 'org.matrix.msc2873.client_theme': params.clientTheme || "", + 'org.matrix.msc2873.client_language': params.clientLanguage || "" + }); + var result = url; + + for (var _i = 0, _Object$keys = Object.keys(variables); _i < _Object$keys.length; _i++) { + var key = _Object$keys[_i]; + // Regex escape from https://stackoverflow.com/a/6969486/7037379 + var pattern = "$".concat(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + + var rexp = new RegExp(pattern, 'g'); // This is technically not what we're supposed to do for a couple reasons: + // 1. We are assuming that there won't later be a $key match after we replace a variable. + // 2. We are assuming that the variable is in a place where it can be escaped (eg: path or query string). + + result = result.replace(rexp, encodeURIComponent(toString(variables[key]))); + } + + return result; +} + +function toString(a) { + if (a === null || a === undefined) { + return "".concat(a); + } + + return a.toString(); +} \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/transport/ITransport.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/transport/ITransport.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/transport/ITransport.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/transport/ITransport.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,5 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/transport/PostmessageTransport.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/transport/PostmessageTransport.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/transport/PostmessageTransport.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/transport/PostmessageTransport.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,258 @@ +"use strict"; + +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PostmessageTransport = void 0; + +var _events = require("events"); + +var _ = require(".."); + +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } + +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } + +function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } + +function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } + +function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } + +function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } + +function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } + +function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +/** + * Transport for the Widget API over postMessage. + */ +var PostmessageTransport = /*#__PURE__*/function (_EventEmitter) { + _inherits(PostmessageTransport, _EventEmitter); + + var _super = _createSuper(PostmessageTransport); + + function PostmessageTransport(sendDirection, initialWidgetId, transportWindow, inboundWindow) { + var _this; + + _classCallCheck(this, PostmessageTransport); + + _this = _super.call(this); + _this.sendDirection = sendDirection; + _this.initialWidgetId = initialWidgetId; + _this.transportWindow = transportWindow; + _this.inboundWindow = inboundWindow; + + _defineProperty(_assertThisInitialized(_this), "strictOriginCheck", void 0); + + _defineProperty(_assertThisInitialized(_this), "targetOrigin", void 0); + + _defineProperty(_assertThisInitialized(_this), "timeoutSeconds", 10); + + _defineProperty(_assertThisInitialized(_this), "_ready", false); + + _defineProperty(_assertThisInitialized(_this), "_widgetId", null); + + _defineProperty(_assertThisInitialized(_this), "outboundRequests", new Map()); + + _defineProperty(_assertThisInitialized(_this), "stopController", new AbortController()); + + _this._widgetId = initialWidgetId; + return _this; + } + + _createClass(PostmessageTransport, [{ + key: "ready", + get: function get() { + return this._ready; + } + }, { + key: "widgetId", + get: function get() { + return this._widgetId || null; + } + }, { + key: "nextRequestId", + get: function get() { + var idBase = "widgetapi-".concat(Date.now()); + var index = 0; + var id = idBase; + + while (this.outboundRequests.has(id)) { + id = "".concat(idBase, "-").concat(index++); + } // reserve the ID + + + this.outboundRequests.set(id, null); + return id; + } + }, { + key: "sendInternal", + value: function sendInternal(message) { + var targetOrigin = this.targetOrigin || '*'; + console.log("[PostmessageTransport] Sending object to ".concat(targetOrigin, ": "), message); + this.transportWindow.postMessage(message, targetOrigin); + } + }, { + key: "reply", + value: function reply(request, responseData) { + return this.sendInternal(_objectSpread(_objectSpread({}, request), {}, { + response: responseData + })); + } + }, { + key: "send", + value: function send(action, data) { + return this.sendComplete(action, data).then(function (r) { + return r.response; + }); + } + }, { + key: "sendComplete", + value: function sendComplete(action, data) { + var _this2 = this; + + if (!this.ready || !this.widgetId) { + return Promise.reject(new Error("Not ready or unknown widget ID")); + } + + var request = { + api: this.sendDirection, + widgetId: this.widgetId, + requestId: this.nextRequestId, + action: action, + data: data + }; + + if (action === _.WidgetApiToWidgetAction.UpdateVisibility) { + // XXX: This is for Scalar support + // TODO: Fix scalar + request['visible'] = data['visible']; + } + + return new Promise(function (prResolve, prReject) { + var resolve = function resolve(response) { + cleanUp(); + prResolve(response); + }; + + var reject = function reject(err) { + cleanUp(); + prReject(err); + }; + + var timerId = setTimeout(function () { + return reject(new Error("Request timed out")); + }, (_this2.timeoutSeconds || 1) * 1000); + + var onStop = function onStop() { + return reject(new Error("Transport stopped")); + }; + + _this2.stopController.signal.addEventListener("abort", onStop); + + var cleanUp = function cleanUp() { + _this2.outboundRequests["delete"](request.requestId); + + clearTimeout(timerId); + + _this2.stopController.signal.removeEventListener("abort", onStop); + }; + + _this2.outboundRequests.set(request.requestId, { + request: request, + resolve: resolve, + reject: reject + }); + + _this2.sendInternal(request); + }); + } + }, { + key: "start", + value: function start() { + var _this3 = this; + + this.inboundWindow.addEventListener("message", function (ev) { + _this3.handleMessage(ev); + }); + this._ready = true; + } + }, { + key: "stop", + value: function stop() { + this._ready = false; + this.stopController.abort(); + } + }, { + key: "handleMessage", + value: function handleMessage(ev) { + if (this.stopController.signal.aborted) return; + if (!ev.data) return; // invalid event + + if (this.strictOriginCheck && ev.origin !== window.origin) return; // bad origin + // treat the message as a response first, then downgrade to a request + + var response = ev.data; + if (!response.action || !response.requestId || !response.widgetId) return; // invalid request/response + + if (!response.response) { + // it's a request + var request = response; + if (request.api !== (0, _.invertedDirection)(this.sendDirection)) return; // wrong direction + + this.handleRequest(request); + } else { + // it's a response + if (response.api !== this.sendDirection) return; // wrong direction + + this.handleResponse(response); + } + } + }, { + key: "handleRequest", + value: function handleRequest(request) { + if (this.widgetId) { + if (this.widgetId !== request.widgetId) return; // wrong widget + } else { + this._widgetId = request.widgetId; + } + + this.emit("message", new CustomEvent("message", { + detail: request + })); + } + }, { + key: "handleResponse", + value: function handleResponse(response) { + if (response.widgetId !== this.widgetId) return; // wrong widget + + var req = this.outboundRequests.get(response.requestId); + if (!req) return; // response to an unknown request + + if ((0, _.isErrorResponse)(response.response)) { + var _err = response.response; + req.reject(new Error(_err.error.message)); + } else { + req.resolve(response); + } + } + }]); + + return PostmessageTransport; +}(_events.EventEmitter); + +exports.PostmessageTransport = PostmessageTransport; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/util/SimpleObservable.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/util/SimpleObservable.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/util/SimpleObservable.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/util/SimpleObservable.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,78 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.SimpleObservable = void 0; + +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var SimpleObservable = /*#__PURE__*/function () { + function SimpleObservable(initialFn) { + _classCallCheck(this, SimpleObservable); + + _defineProperty(this, "listeners", []); + + if (initialFn) this.listeners.push(initialFn); + } + + _createClass(SimpleObservable, [{ + key: "onUpdate", + value: function onUpdate(fn) { + this.listeners.push(fn); + } + }, { + key: "update", + value: function update(val) { + var _iterator = _createForOfIteratorHelper(this.listeners), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var listener = _step.value; + listener(val); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + } + }, { + key: "close", + value: function close() { + this.listeners = []; // reset + } + }]); + + return SimpleObservable; +}(); + +exports.SimpleObservable = SimpleObservable; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/WidgetApi.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/WidgetApi.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/matrix-widget-api/WidgetApi.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/matrix-widget-api/WidgetApi.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,872 @@ +"use strict"; + +function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.WidgetApi = void 0; + +var _events = require("events"); + +var _WidgetApiDirection = require("./interfaces/WidgetApiDirection"); + +var _ApiVersion = require("./interfaces/ApiVersion"); + +var _PostmessageTransport = require("./transport/PostmessageTransport"); + +var _WidgetApiAction = require("./interfaces/WidgetApiAction"); + +var _GetOpenIDAction = require("./interfaces/GetOpenIDAction"); + +var _WidgetType = require("./interfaces/WidgetType"); + +var _ModalWidgetActions = require("./interfaces/ModalWidgetActions"); + +var _WidgetEventCapability = require("./models/WidgetEventCapability"); + +var _Symbols = require("./Symbols"); + +function _regeneratorRuntime() { "use strict"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime = function _regeneratorRuntime() { return exports; }; var exports = {}, Op = Object.prototype, hasOwn = Op.hasOwnProperty, $Symbol = "function" == typeof Symbol ? Symbol : {}, iteratorSymbol = $Symbol.iterator || "@@iterator", asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator", toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; function define(obj, key, value) { return Object.defineProperty(obj, key, { value: value, enumerable: !0, configurable: !0, writable: !0 }), obj[key]; } try { define({}, ""); } catch (err) { define = function define(obj, key, value) { return obj[key] = value; }; } function wrap(innerFn, outerFn, self, tryLocsList) { var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator, generator = Object.create(protoGenerator.prototype), context = new Context(tryLocsList || []); return generator._invoke = function (innerFn, self, context) { var state = "suspendedStart"; return function (method, arg) { if ("executing" === state) throw new Error("Generator is already running"); if ("completed" === state) { if ("throw" === method) throw arg; return doneResult(); } for (context.method = method, context.arg = arg;;) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if ("next" === context.method) context.sent = context._sent = context.arg;else if ("throw" === context.method) { if ("suspendedStart" === state) throw state = "completed", context.arg; context.dispatchException(context.arg); } else "return" === context.method && context.abrupt("return", context.arg); state = "executing"; var record = tryCatch(innerFn, self, context); if ("normal" === record.type) { if (state = context.done ? "completed" : "suspendedYield", record.arg === ContinueSentinel) continue; return { value: record.arg, done: context.done }; } "throw" === record.type && (state = "completed", context.method = "throw", context.arg = record.arg); } }; }(innerFn, self, context), generator; } function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } exports.wrap = wrap; var ContinueSentinel = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} var IteratorPrototype = {}; define(IteratorPrototype, iteratorSymbol, function () { return this; }); var getProto = Object.getPrototypeOf, NativeIteratorPrototype = getProto && getProto(getProto(values([]))); NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol) && (IteratorPrototype = NativeIteratorPrototype); var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function (method) { define(prototype, method, function (arg) { return this._invoke(method, arg); }); }); } function AsyncIterator(generator, PromiseImpl) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if ("throw" !== record.type) { var result = record.arg, value = result.value; return value && "object" == _typeof(value) && hasOwn.call(value, "__await") ? PromiseImpl.resolve(value.__await).then(function (value) { invoke("next", value, resolve, reject); }, function (err) { invoke("throw", err, resolve, reject); }) : PromiseImpl.resolve(value).then(function (unwrapped) { result.value = unwrapped, resolve(result); }, function (error) { return invoke("throw", error, resolve, reject); }); } reject(record.arg); } var previousPromise; this._invoke = function (method, arg) { function callInvokeWithMethodAndArg() { return new PromiseImpl(function (resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = previousPromise ? previousPromise.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); }; } function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (undefined === method) { if (context.delegate = null, "throw" === context.method) { if (delegate.iterator["return"] && (context.method = "return", context.arg = undefined, maybeInvokeDelegate(delegate, context), "throw" === context.method)) return ContinueSentinel; context.method = "throw", context.arg = new TypeError("The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if ("throw" === record.type) return context.method = "throw", context.arg = record.arg, context.delegate = null, ContinueSentinel; var info = record.arg; return info ? info.done ? (context[delegate.resultName] = info.value, context.next = delegate.nextLoc, "return" !== context.method && (context.method = "next", context.arg = undefined), context.delegate = null, ContinueSentinel) : info : (context.method = "throw", context.arg = new TypeError("iterator result is not an object"), context.delegate = null, ContinueSentinel); } function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; 1 in locs && (entry.catchLoc = locs[1]), 2 in locs && (entry.finallyLoc = locs[2], entry.afterLoc = locs[3]), this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal", delete record.arg, entry.completion = record; } function Context(tryLocsList) { this.tryEntries = [{ tryLoc: "root" }], tryLocsList.forEach(pushTryEntry, this), this.reset(!0); } function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) return iteratorMethod.call(iterable); if ("function" == typeof iterable.next) return iterable; if (!isNaN(iterable.length)) { var i = -1, next = function next() { for (; ++i < iterable.length;) { if (hasOwn.call(iterable, i)) return next.value = iterable[i], next.done = !1, next; } return next.value = undefined, next.done = !0, next; }; return next.next = next; } } return { next: doneResult }; } function doneResult() { return { value: undefined, done: !0 }; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, define(Gp, "constructor", GeneratorFunctionPrototype), define(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, toStringTagSymbol, "GeneratorFunction"), exports.isGeneratorFunction = function (genFun) { var ctor = "function" == typeof genFun && genFun.constructor; return !!ctor && (ctor === GeneratorFunction || "GeneratorFunction" === (ctor.displayName || ctor.name)); }, exports.mark = function (genFun) { return Object.setPrototypeOf ? Object.setPrototypeOf(genFun, GeneratorFunctionPrototype) : (genFun.__proto__ = GeneratorFunctionPrototype, define(genFun, toStringTagSymbol, "GeneratorFunction")), genFun.prototype = Object.create(Gp), genFun; }, exports.awrap = function (arg) { return { __await: arg }; }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, asyncIteratorSymbol, function () { return this; }), exports.AsyncIterator = AsyncIterator, exports.async = function (innerFn, outerFn, self, tryLocsList, PromiseImpl) { void 0 === PromiseImpl && (PromiseImpl = Promise); var iter = new AsyncIterator(wrap(innerFn, outerFn, self, tryLocsList), PromiseImpl); return exports.isGeneratorFunction(outerFn) ? iter : iter.next().then(function (result) { return result.done ? result.value : iter.next(); }); }, defineIteratorMethods(Gp), define(Gp, toStringTagSymbol, "Generator"), define(Gp, iteratorSymbol, function () { return this; }), define(Gp, "toString", function () { return "[object Generator]"; }), exports.keys = function (object) { var keys = []; for (var key in object) { keys.push(key); } return keys.reverse(), function next() { for (; keys.length;) { var key = keys.pop(); if (key in object) return next.value = key, next.done = !1, next; } return next.done = !0, next; }; }, exports.values = values, Context.prototype = { constructor: Context, reset: function reset(skipTempReset) { if (this.prev = 0, this.next = 0, this.sent = this._sent = undefined, this.done = !1, this.delegate = null, this.method = "next", this.arg = undefined, this.tryEntries.forEach(resetTryEntry), !skipTempReset) for (var name in this) { "t" === name.charAt(0) && hasOwn.call(this, name) && !isNaN(+name.slice(1)) && (this[name] = undefined); } }, stop: function stop() { this.done = !0; var rootRecord = this.tryEntries[0].completion; if ("throw" === rootRecord.type) throw rootRecord.arg; return this.rval; }, dispatchException: function dispatchException(exception) { if (this.done) throw exception; var context = this; function handle(loc, caught) { return record.type = "throw", record.arg = exception, context.next = loc, caught && (context.method = "next", context.arg = undefined), !!caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i], record = entry.completion; if ("root" === entry.tryLoc) return handle("end"); if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"), hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) return handle(entry.catchLoc, !0); if (this.prev < entry.finallyLoc) return handle(entry.finallyLoc); } else if (hasCatch) { if (this.prev < entry.catchLoc) return handle(entry.catchLoc, !0); } else { if (!hasFinally) throw new Error("try statement without catch or finally"); if (this.prev < entry.finallyLoc) return handle(entry.finallyLoc); } } } }, abrupt: function abrupt(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } finallyEntry && ("break" === type || "continue" === type) && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc && (finallyEntry = null); var record = finallyEntry ? finallyEntry.completion : {}; return record.type = type, record.arg = arg, finallyEntry ? (this.method = "next", this.next = finallyEntry.finallyLoc, ContinueSentinel) : this.complete(record); }, complete: function complete(record, afterLoc) { if ("throw" === record.type) throw record.arg; return "break" === record.type || "continue" === record.type ? this.next = record.arg : "return" === record.type ? (this.rval = this.arg = record.arg, this.method = "return", this.next = "end") : "normal" === record.type && afterLoc && (this.next = afterLoc), ContinueSentinel; }, finish: function finish(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) return this.complete(entry.completion, entry.afterLoc), resetTryEntry(entry), ContinueSentinel; } }, "catch": function _catch(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if ("throw" === record.type) { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } throw new Error("illegal catch attempt"); }, delegateYield: function delegateYield(iterable, resultName, nextLoc) { return this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }, "next" === this.method && (this.arg = undefined), ContinueSentinel; } }, exports; } + +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } + +function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } + +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } + +function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } + +function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } + +function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } + +function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } + +function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } + +function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _awaitAsyncGenerator(value) { return new _AwaitValue(value); } + +function _wrapAsyncGenerator(fn) { return function () { return new _AsyncGenerator(fn.apply(this, arguments)); }; } + +function _AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; var wrappedAwait = value instanceof _AwaitValue; Promise.resolve(wrappedAwait ? value.wrapped : value).then(function (arg) { if (wrappedAwait) { resume(key === "return" ? "return" : "next", arg); return; } settle(result.done ? "return" : "normal", arg); }, function (err) { resume("throw", err); }); } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen["return"] !== "function") { this["return"] = undefined; } } + +_AsyncGenerator.prototype[typeof Symbol === "function" && Symbol.asyncIterator || "@@asyncIterator"] = function () { return this; }; + +_AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); }; + +_AsyncGenerator.prototype["throw"] = function (arg) { return this._invoke("throw", arg); }; + +_AsyncGenerator.prototype["return"] = function (arg) { return this._invoke("return", arg); }; + +function _AwaitValue(value) { this.wrapped = value; } + +/** + * API handler for widgets. This raises events for each action + * received as `action:${action}` (eg: "action:screenshot"). + * Default handling can be prevented by using preventDefault() + * on the raised event. The default handling varies for each + * action: ones which the SDK can handle safely are acknowledged + * appropriately and ones which are unhandled (custom or require + * the widget to do something) are rejected with an error. + * + * Events which are preventDefault()ed must reply using the + * transport. The events raised will have a detail of an + * IWidgetApiRequest interface. + * + * When the WidgetApi is ready to start sending requests, it will + * raise a "ready" CustomEvent. After the ready event fires, actions + * can be sent and the transport will be ready. + */ +var WidgetApi = /*#__PURE__*/function (_EventEmitter) { + _inherits(WidgetApi, _EventEmitter); + + var _super = _createSuper(WidgetApi); + + /** + * Creates a new API handler for the given widget. + * @param {string} widgetId The widget ID to listen for. If not supplied then + * the API will use the widget ID from the first valid request it receives. + * @param {string} clientOrigin The origin of the client, or null if not known. + */ + function WidgetApi() { + var _this2; + + var widgetId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var clientOrigin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + + _classCallCheck(this, WidgetApi); + + _this2 = _super.call(this); + _this2.clientOrigin = clientOrigin; + + _defineProperty(_assertThisInitialized(_this2), "transport", void 0); + + _defineProperty(_assertThisInitialized(_this2), "capabilitiesFinished", false); + + _defineProperty(_assertThisInitialized(_this2), "supportsMSC2974Renegotiate", false); + + _defineProperty(_assertThisInitialized(_this2), "requestedCapabilities", []); + + _defineProperty(_assertThisInitialized(_this2), "approvedCapabilities", void 0); + + _defineProperty(_assertThisInitialized(_this2), "cachedClientVersions", void 0); + + _defineProperty(_assertThisInitialized(_this2), "turnServerWatchers", 0); + + if (!window.parent) { + throw new Error("No parent window. This widget doesn't appear to be embedded properly."); + } + + _this2.transport = new _PostmessageTransport.PostmessageTransport(_WidgetApiDirection.WidgetApiDirection.FromWidget, widgetId, window.parent, window); + _this2.transport.targetOrigin = clientOrigin; + + _this2.transport.on("message", _this2.handleMessage.bind(_assertThisInitialized(_this2))); + + return _this2; + } + /** + * Determines if the widget was granted a particular capability. Note that on + * clients where the capabilities are not fed back to the widget this function + * will rely on requested capabilities instead. + * @param {Capability} capability The capability to check for approval of. + * @returns {boolean} True if the widget has approval for the given capability. + */ + + + _createClass(WidgetApi, [{ + key: "hasCapability", + value: function hasCapability(capability) { + if (Array.isArray(this.approvedCapabilities)) { + return this.approvedCapabilities.includes(capability); + } + + return this.requestedCapabilities.includes(capability); + } + /** + * Request a capability from the client. It is not guaranteed to be allowed, + * but will be asked for. + * @param {Capability} capability The capability to request. + * @throws Throws if the capabilities negotiation has already started and the + * widget is unable to request additional capabilities. + */ + + }, { + key: "requestCapability", + value: function requestCapability(capability) { + if (this.capabilitiesFinished && !this.supportsMSC2974Renegotiate) { + throw new Error("Capabilities have already been negotiated"); + } + + this.requestedCapabilities.push(capability); + } + /** + * Request capabilities from the client. They are not guaranteed to be allowed, + * but will be asked for if the negotiation has not already happened. + * @param {Capability[]} capabilities The capabilities to request. + * @throws Throws if the capabilities negotiation has already started. + */ + + }, { + key: "requestCapabilities", + value: function requestCapabilities(capabilities) { + var _this3 = this; + + capabilities.forEach(function (cap) { + return _this3.requestCapability(cap); + }); + } + /** + * Requests the capability to interact with rooms other than the user's currently + * viewed room. Applies to event receiving and sending. + * @param {string | Symbols.AnyRoom} roomId The room ID, or `Symbols.AnyRoom` to + * denote all known rooms. + */ + + }, { + key: "requestCapabilityForRoomTimeline", + value: function requestCapabilityForRoomTimeline(roomId) { + this.requestCapability("org.matrix.msc2762.timeline:".concat(roomId)); + } + /** + * Requests the capability to send a given state event with optional explicit + * state key. It is not guaranteed to be allowed, but will be asked for if the + * negotiation has not already happened. + * @param {string} eventType The state event type to ask for. + * @param {string} stateKey If specified, the specific state key to request. + * Otherwise all state keys will be requested. + */ + + }, { + key: "requestCapabilityToSendState", + value: function requestCapabilityToSendState(eventType, stateKey) { + this.requestCapability(_WidgetEventCapability.WidgetEventCapability.forStateEvent(_WidgetEventCapability.EventDirection.Send, eventType, stateKey).raw); + } + /** + * Requests the capability to receive a given state event with optional explicit + * state key. It is not guaranteed to be allowed, but will be asked for if the + * negotiation has not already happened. + * @param {string} eventType The state event type to ask for. + * @param {string} stateKey If specified, the specific state key to request. + * Otherwise all state keys will be requested. + */ + + }, { + key: "requestCapabilityToReceiveState", + value: function requestCapabilityToReceiveState(eventType, stateKey) { + this.requestCapability(_WidgetEventCapability.WidgetEventCapability.forStateEvent(_WidgetEventCapability.EventDirection.Receive, eventType, stateKey).raw); + } + /** + * Requests the capability to send a given to-device event. It is not + * guaranteed to be allowed, but will be asked for if the negotiation has + * not already happened. + * @param {string} eventType The room event type to ask for. + */ + + }, { + key: "requestCapabilityToSendToDevice", + value: function requestCapabilityToSendToDevice(eventType) { + this.requestCapability(_WidgetEventCapability.WidgetEventCapability.forToDeviceEvent(_WidgetEventCapability.EventDirection.Send, eventType).raw); + } + /** + * Requests the capability to receive a given to-device event. It is not + * guaranteed to be allowed, but will be asked for if the negotiation has + * not already happened. + * @param {string} eventType The room event type to ask for. + */ + + }, { + key: "requestCapabilityToReceiveToDevice", + value: function requestCapabilityToReceiveToDevice(eventType) { + this.requestCapability(_WidgetEventCapability.WidgetEventCapability.forToDeviceEvent(_WidgetEventCapability.EventDirection.Receive, eventType).raw); + } + /** + * Requests the capability to send a given room event. It is not guaranteed to be + * allowed, but will be asked for if the negotiation has not already happened. + * @param {string} eventType The room event type to ask for. + */ + + }, { + key: "requestCapabilityToSendEvent", + value: function requestCapabilityToSendEvent(eventType) { + this.requestCapability(_WidgetEventCapability.WidgetEventCapability.forRoomEvent(_WidgetEventCapability.EventDirection.Send, eventType).raw); + } + /** + * Requests the capability to receive a given room event. It is not guaranteed to be + * allowed, but will be asked for if the negotiation has not already happened. + * @param {string} eventType The room event type to ask for. + */ + + }, { + key: "requestCapabilityToReceiveEvent", + value: function requestCapabilityToReceiveEvent(eventType) { + this.requestCapability(_WidgetEventCapability.WidgetEventCapability.forRoomEvent(_WidgetEventCapability.EventDirection.Receive, eventType).raw); + } + /** + * Requests the capability to send a given message event with optional explicit + * `msgtype`. It is not guaranteed to be allowed, but will be asked for if the + * negotiation has not already happened. + * @param {string} msgtype If specified, the specific msgtype to request. + * Otherwise all message types will be requested. + */ + + }, { + key: "requestCapabilityToSendMessage", + value: function requestCapabilityToSendMessage(msgtype) { + this.requestCapability(_WidgetEventCapability.WidgetEventCapability.forRoomMessageEvent(_WidgetEventCapability.EventDirection.Send, msgtype).raw); + } + /** + * Requests the capability to receive a given message event with optional explicit + * `msgtype`. It is not guaranteed to be allowed, but will be asked for if the + * negotiation has not already happened. + * @param {string} msgtype If specified, the specific msgtype to request. + * Otherwise all message types will be requested. + */ + + }, { + key: "requestCapabilityToReceiveMessage", + value: function requestCapabilityToReceiveMessage(msgtype) { + this.requestCapability(_WidgetEventCapability.WidgetEventCapability.forRoomMessageEvent(_WidgetEventCapability.EventDirection.Receive, msgtype).raw); + } + /** + * Requests an OpenID Connect token from the client for the currently logged in + * user. This token can be validated server-side with the federation API. Note + * that the widget is responsible for validating the token and caching any results + * it needs. + * @returns {Promise} Resolves to a token for verification. + * @throws Throws if the user rejected the request or the request failed. + */ + + }, { + key: "requestOpenIDConnectToken", + value: function requestOpenIDConnectToken() { + var _this4 = this; + + return new Promise(function (resolve, reject) { + _this4.transport.sendComplete(_WidgetApiAction.WidgetApiFromWidgetAction.GetOpenIDCredentials, {}).then(function (response) { + var rdata = response.response; + + if (rdata.state === _GetOpenIDAction.OpenIDRequestState.Allowed) { + resolve(rdata); + } else if (rdata.state === _GetOpenIDAction.OpenIDRequestState.Blocked) { + reject(new Error("User declined to verify their identity")); + } else if (rdata.state === _GetOpenIDAction.OpenIDRequestState.PendingUserConfirmation) { + var handlerFn = function handlerFn(ev) { + ev.preventDefault(); + var request = ev.detail; + if (request.data.original_request_id !== response.requestId) return; + + if (request.data.state === _GetOpenIDAction.OpenIDRequestState.Allowed) { + resolve(request.data); + + _this4.transport.reply(request, {}); // ack + + } else if (request.data.state === _GetOpenIDAction.OpenIDRequestState.Blocked) { + reject(new Error("User declined to verify their identity")); + + _this4.transport.reply(request, {}); // ack + + } else { + reject(new Error("Invalid state on reply: " + rdata.state)); + + _this4.transport.reply(request, { + error: { + message: "Invalid state" + } + }); + } + + _this4.off("action:".concat(_WidgetApiAction.WidgetApiToWidgetAction.OpenIDCredentials), handlerFn); + }; + + _this4.on("action:".concat(_WidgetApiAction.WidgetApiToWidgetAction.OpenIDCredentials), handlerFn); + } else { + reject(new Error("Invalid state: " + rdata.state)); + } + })["catch"](reject); + }); + } + /** + * Asks the client for additional capabilities. Capabilities can be queued for this + * request with the requestCapability() functions. + * @returns {Promise} Resolves when complete. Note that the promise resolves when + * the capabilities request has gone through, not when the capabilities are approved/denied. + * Use the WidgetApiToWidgetAction.NotifyCapabilities action to detect changes. + */ + + }, { + key: "updateRequestedCapabilities", + value: function updateRequestedCapabilities() { + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.MSC2974RenegotiateCapabilities, { + capabilities: this.requestedCapabilities + }).then(); + } + /** + * Tell the client that the content has been loaded. + * @returns {Promise} Resolves when the client acknowledges the request. + */ + + }, { + key: "sendContentLoaded", + value: function sendContentLoaded() { + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.ContentLoaded, {}).then(); + } + /** + * Sends a sticker to the client. + * @param {IStickerActionRequestData} sticker The sticker to send. + * @returns {Promise} Resolves when the client acknowledges the request. + */ + + }, { + key: "sendSticker", + value: function sendSticker(sticker) { + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.SendSticker, sticker).then(); + } + /** + * Asks the client to set the always-on-screen status for this widget. + * @param {boolean} value The new state to request. + * @returns {Promise} Resolve with true if the client was able to fulfill + * the request, resolves to false otherwise. Rejects if an error occurred. + */ + + }, { + key: "setAlwaysOnScreen", + value: function setAlwaysOnScreen(value) { + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.UpdateAlwaysOnScreen, { + value: value + }).then(function (res) { + return res.success; + }); + } + /** + * Opens a modal widget. + * @param {string} url The URL to the modal widget. + * @param {string} name The name of the widget. + * @param {IModalWidgetOpenRequestDataButton[]} buttons The buttons to have on the widget. + * @param {IModalWidgetCreateData} data Data to supply to the modal widget. + * @param {WidgetType} type The type of modal widget. + * @returns {Promise} Resolves when the modal widget has been opened. + */ + + }, { + key: "openModalWidget", + value: function openModalWidget(url, name) { + var buttons = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; + var data = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + var type = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : _WidgetType.MatrixWidgetType.Custom; + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.OpenModalWidget, { + type: type, + url: url, + name: name, + buttons: buttons, + data: data + }).then(); + } + /** + * Closes the modal widget. The widget's session will be terminated shortly after. + * @param {IModalWidgetReturnData} data Optional data to close the modal widget with. + * @returns {Promise} Resolves when complete. + */ + + }, { + key: "closeModalWidget", + value: function closeModalWidget() { + var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.CloseModalWidget, data).then(); + } + }, { + key: "sendRoomEvent", + value: function sendRoomEvent(eventType, content, roomId) { + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.SendEvent, { + type: eventType, + content: content, + room_id: roomId + }); + } + }, { + key: "sendStateEvent", + value: function sendStateEvent(eventType, stateKey, content, roomId) { + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.SendEvent, { + type: eventType, + content: content, + state_key: stateKey, + room_id: roomId + }); + } + /** + * Sends a to-device event. + * @param {string} eventType The type of events being sent. + * @param {boolean} encrypted Whether to encrypt the message contents. + * @param {Object} contentMap A map from user IDs to device IDs to message contents. + * @returns {Promise} Resolves when complete. + */ + + }, { + key: "sendToDevice", + value: function sendToDevice(eventType, encrypted, contentMap) { + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.SendToDevice, { + type: eventType, + encrypted: encrypted, + messages: contentMap + }); + } + }, { + key: "readRoomEvents", + value: function readRoomEvents(eventType, limit, msgtype, roomIds) { + var data = { + type: eventType, + msgtype: msgtype + }; + + if (limit !== undefined) { + data.limit = limit; + } + + if (roomIds) { + if (roomIds.includes(_Symbols.Symbols.AnyRoom)) { + data.room_ids = _Symbols.Symbols.AnyRoom; + } else { + data.room_ids = roomIds; + } + } + + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.MSC2876ReadEvents, data).then(function (r) { + return r.events; + }); + } + /** + * Reads all related events given a known eventId. + * @param eventId The id of the parent event to be read. + * @param roomId The room to look within. When undefined, the user's currently + * viewed room. + * @param relationType The relationship type of child events to search for. + * When undefined, all relations are returned. + * @param eventType The event type of child events to search for. When undefined, + * all related events are returned. + * @param limit The maximum number of events to retrieve per room. If not + * supplied, the server will apply a default limit. + * @param from The pagination token to start returning results from, as + * received from a previous call. If not supplied, results start at the most + * recent topological event known to the server. + * @param to The pagination token to stop returning results at. If not + * supplied, results continue up to limit or until there are no more events. + * @param direction The direction to search for according to MSC3715. + * @returns Resolves to the room relations. + */ + + }, { + key: "readEventRelations", + value: function () { + var _readEventRelations = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(eventId, roomId, relationType, eventType, limit, from, to, direction) { + var versions, data; + return _regeneratorRuntime().wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + _context.next = 2; + return this.getClientVersions(); + + case 2: + versions = _context.sent; + + if (versions.includes(_ApiVersion.UnstableApiVersion.MSC3869)) { + _context.next = 5; + break; + } + + throw new Error("The read_relations action is not supported by the client."); + + case 5: + data = { + event_id: eventId, + rel_type: relationType, + event_type: eventType, + room_id: roomId, + to: to, + from: from, + limit: limit, + direction: direction + }; + return _context.abrupt("return", this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.MSC3869ReadRelations, data)); + + case 7: + case "end": + return _context.stop(); + } + } + }, _callee, this); + })); + + function readEventRelations(_x, _x2, _x3, _x4, _x5, _x6, _x7, _x8) { + return _readEventRelations.apply(this, arguments); + } + + return readEventRelations; + }() + }, { + key: "readStateEvents", + value: function readStateEvents(eventType, limit, stateKey, roomIds) { + var data = { + type: eventType, + state_key: stateKey === undefined ? true : stateKey + }; + + if (limit !== undefined) { + data.limit = limit; + } + + if (roomIds) { + if (roomIds.includes(_Symbols.Symbols.AnyRoom)) { + data.room_ids = _Symbols.Symbols.AnyRoom; + } else { + data.room_ids = roomIds; + } + } + + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.MSC2876ReadEvents, data).then(function (r) { + return r.events; + }); + } + /** + * Sets a button as disabled or enabled on the modal widget. Buttons are enabled by default. + * @param {ModalButtonID} buttonId The button ID to enable/disable. + * @param {boolean} isEnabled Whether or not the button is enabled. + * @returns {Promise} Resolves when complete. + * @throws Throws if the button cannot be disabled, or the client refuses to disable the button. + */ + + }, { + key: "setModalButtonEnabled", + value: function setModalButtonEnabled(buttonId, isEnabled) { + if (buttonId === _ModalWidgetActions.BuiltInModalButtonID.Close) { + throw new Error("The close button cannot be disabled"); + } + + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.SetModalButtonEnabled, { + button: buttonId, + enabled: isEnabled + }).then(); + } + /** + * Attempts to navigate the client to the given URI. This can only be called with Matrix URIs + * (currently only matrix.to, but in future a Matrix URI scheme will be defined). + * @param {string} uri The URI to navigate to. + * @returns {Promise} Resolves when complete. + * @throws Throws if the URI is invalid or cannot be processed. + * @deprecated This currently relies on an unstable MSC (MSC2931). + */ + + }, { + key: "navigateTo", + value: function navigateTo(uri) { + if (!uri || !uri.startsWith("https://matrix.to/#")) { + throw new Error("Invalid matrix.to URI"); + } + + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.MSC2931Navigate, { + uri: uri + }).then(); + } + /** + * Starts watching for TURN servers, yielding an initial set of credentials as soon as possible, + * and thereafter yielding new credentials whenever the previous ones expire. + * @yields {ITurnServer} The TURN server URIs and credentials currently available to the widget. + */ + + }, { + key: "getTurnServers", + value: function getTurnServers() { + var _this = this; + + return _wrapAsyncGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3() { + var setTurnServer, onUpdateTurnServers; + return _regeneratorRuntime().wrap(function _callee3$(_context3) { + while (1) { + switch (_context3.prev = _context3.next) { + case 0: + onUpdateTurnServers = /*#__PURE__*/function () { + var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2(ev) { + return _regeneratorRuntime().wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + ev.preventDefault(); + setTurnServer(ev.detail.data); + _context2.next = 4; + return _this.transport.reply(ev.detail, {}); + + case 4: + case "end": + return _context2.stop(); + } + } + }, _callee2); + })); + + return function onUpdateTurnServers(_x9) { + return _ref.apply(this, arguments); + }; + }(); // Start listening for updates before we even start watching, to catch + // TURN data that is sent immediately + + + _this.on("action:".concat(_WidgetApiAction.WidgetApiToWidgetAction.UpdateTurnServers), onUpdateTurnServers); // Only send the 'watch' action if we aren't already watching + + + if (!(_this.turnServerWatchers === 0)) { + _context3.next = 12; + break; + } + + _context3.prev = 3; + _context3.next = 6; + return _awaitAsyncGenerator(_this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.WatchTurnServers, {})); + + case 6: + _context3.next = 12; + break; + + case 8: + _context3.prev = 8; + _context3.t0 = _context3["catch"](3); + + _this.off("action:".concat(_WidgetApiAction.WidgetApiToWidgetAction.UpdateTurnServers), onUpdateTurnServers); + + throw _context3.t0; + + case 12: + _this.turnServerWatchers++; + _context3.prev = 13; + + case 14: + if (!true) { + _context3.next = 21; + break; + } + + _context3.next = 17; + return _awaitAsyncGenerator(new Promise(function (resolve) { + return setTurnServer = resolve; + })); + + case 17: + _context3.next = 19; + return _context3.sent; + + case 19: + _context3.next = 14; + break; + + case 21: + _context3.prev = 21; + + // The loop was broken by the caller - clean up + _this.off("action:".concat(_WidgetApiAction.WidgetApiToWidgetAction.UpdateTurnServers), onUpdateTurnServers); // Since sending the 'unwatch' action will end updates for all other + // consumers, only send it if we're the only consumer remaining + + + _this.turnServerWatchers--; + + if (!(_this.turnServerWatchers === 0)) { + _context3.next = 27; + break; + } + + _context3.next = 27; + return _awaitAsyncGenerator(_this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.UnwatchTurnServers, {})); + + case 27: + return _context3.finish(21); + + case 28: + case "end": + return _context3.stop(); + } + } + }, _callee3, null, [[3, 8], [13,, 21, 28]]); + }))(); + } + /** + * Starts the communication channel. This should be done early to ensure + * that messages are not missed. Communication can only be stopped by the client. + */ + + }, { + key: "start", + value: function start() { + var _this5 = this; + + this.transport.start(); + this.getClientVersions().then(function (v) { + if (v.includes(_ApiVersion.UnstableApiVersion.MSC2974)) { + _this5.supportsMSC2974Renegotiate = true; + } + }); + } + }, { + key: "handleMessage", + value: function handleMessage(ev) { + var actionEv = new CustomEvent("action:".concat(ev.detail.action), { + detail: ev.detail, + cancelable: true + }); + this.emit("action:".concat(ev.detail.action), actionEv); + + if (!actionEv.defaultPrevented) { + switch (ev.detail.action) { + case _WidgetApiAction.WidgetApiToWidgetAction.SupportedApiVersions: + return this.replyVersions(ev.detail); + + case _WidgetApiAction.WidgetApiToWidgetAction.Capabilities: + return this.handleCapabilities(ev.detail); + + case _WidgetApiAction.WidgetApiToWidgetAction.UpdateVisibility: + return this.transport.reply(ev.detail, {}); + // ack to avoid error spam + + case _WidgetApiAction.WidgetApiToWidgetAction.NotifyCapabilities: + return this.transport.reply(ev.detail, {}); + // ack to avoid error spam + + default: + return this.transport.reply(ev.detail, { + error: { + message: "Unknown or unsupported action: " + ev.detail.action + } + }); + } + } + } + }, { + key: "replyVersions", + value: function replyVersions(request) { + this.transport.reply(request, { + supported_versions: _ApiVersion.CurrentApiVersions + }); + } + }, { + key: "getClientVersions", + value: function getClientVersions() { + var _this6 = this; + + if (Array.isArray(this.cachedClientVersions)) { + return Promise.resolve(this.cachedClientVersions); + } + + return this.transport.send(_WidgetApiAction.WidgetApiFromWidgetAction.SupportedApiVersions, {}).then(function (r) { + _this6.cachedClientVersions = r.supported_versions; + return r.supported_versions; + })["catch"](function (e) { + console.warn("non-fatal error getting supported client versions: ", e); + return []; + }); + } + }, { + key: "handleCapabilities", + value: function handleCapabilities(request) { + var _this7 = this; + + if (this.capabilitiesFinished) { + return this.transport.reply(request, { + error: { + message: "Capability negotiation already completed" + } + }); + } // See if we can expect a capabilities notification or not + + + return this.getClientVersions().then(function (v) { + if (v.includes(_ApiVersion.UnstableApiVersion.MSC2871)) { + _this7.once("action:".concat(_WidgetApiAction.WidgetApiToWidgetAction.NotifyCapabilities), function (ev) { + _this7.approvedCapabilities = ev.detail.data.approved; + + _this7.emit("ready"); + }); + } else { + // if we can't expect notification, we're as done as we can be + _this7.emit("ready"); + } // in either case, reply to that capabilities request + + + _this7.capabilitiesFinished = true; + return _this7.transport.reply(request, { + capabilities: _this7.requestedCapabilities + }); + }); + } + }]); + + return WidgetApi; +}(_events.EventEmitter); + +exports.WidgetApi = WidgetApi; \ No newline at end of file diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/moz.build thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/moz.build --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/moz.build 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/moz.build 2023-04-11 06:11:52.000000000 +0000 @@ -10,11 +10,12 @@ "matrix-sdk/client.js", "matrix-sdk/content-helpers.js", "matrix-sdk/content-repo.js", + "matrix-sdk/embedded.js", "matrix-sdk/errors.js", "matrix-sdk/event-mapper.js", + "matrix-sdk/feature.js", "matrix-sdk/filter-component.js", "matrix-sdk/filter.js", - "matrix-sdk/http-api.js", "matrix-sdk/indexeddb-helpers.js", "matrix-sdk/indexeddb-worker.js", "matrix-sdk/interactive-auth.js", @@ -42,6 +43,7 @@ "matrix-sdk/crypto/api.js", "matrix-sdk/crypto/backup.js", "matrix-sdk/crypto/CrossSigning.js", + "matrix-sdk/crypto/crypto.js", "matrix-sdk/crypto/dehydration.js", "matrix-sdk/crypto/deviceinfo.js", "matrix-sdk/crypto/DeviceList.js", @@ -76,6 +78,7 @@ "matrix-sdk/crypto/verification/IllegalMethod.js", "matrix-sdk/crypto/verification/QRCode.js", "matrix-sdk/crypto/verification/SAS.js", + "matrix-sdk/crypto/verification/SASDecimal.js", ] EXTRA_JS_MODULES.matrix.matrix_sdk.crypto.verification.request += [ @@ -84,6 +87,26 @@ "matrix-sdk/crypto/verification/request/VerificationRequest.js", ] +EXTRA_JS_MODULES.matrix.matrix_sdk.extensible_events_v1 += [ + "matrix-sdk/extensible_events_v1/ExtensibleEvent.js", + "matrix-sdk/extensible_events_v1/InvalidEventError.js", + "matrix-sdk/extensible_events_v1/MessageEvent.js", + "matrix-sdk/extensible_events_v1/PollEndEvent.js", + "matrix-sdk/extensible_events_v1/PollResponseEvent.js", + "matrix-sdk/extensible_events_v1/PollStartEvent.js", + "matrix-sdk/extensible_events_v1/utilities.js", +] + +EXTRA_JS_MODULES.matrix.matrix_sdk.http_api += [ + "matrix-sdk/http-api/errors.js", + "matrix-sdk/http-api/fetch.js", + "matrix-sdk/http-api/index.js", + "matrix-sdk/http-api/interface.js", + "matrix-sdk/http-api/method.js", + "matrix-sdk/http-api/prefix.js", + "matrix-sdk/http-api/utils.js", +] + EXTRA_JS_MODULES.matrix.matrix_sdk.models += [ "matrix-sdk/models/beacon.js", "matrix-sdk/models/event-context.js", @@ -94,6 +117,8 @@ "matrix-sdk/models/invites-ignorer.js", "matrix-sdk/models/MSC3089Branch.js", "matrix-sdk/models/MSC3089TreeSpace.js", + "matrix-sdk/models/poll.js", + "matrix-sdk/models/read-receipt.js", "matrix-sdk/models/related-relations.js", "matrix-sdk/models/relations-container.js", "matrix-sdk/models/relations.js", @@ -107,6 +132,34 @@ "matrix-sdk/models/user.js", ] +EXTRA_JS_MODULES.matrix.matrix_sdk.rendezvous += [ + "matrix-sdk/rendezvous/index.js", + "matrix-sdk/rendezvous/MSC3906Rendezvous.js", + "matrix-sdk/rendezvous/RendezvousError.js", + "matrix-sdk/rendezvous/RendezvousFailureReason.js", + "matrix-sdk/rendezvous/RendezvousIntent.js", +] + +EXTRA_JS_MODULES.matrix.matrix_sdk.rendezvous.channels += [ + "matrix-sdk/rendezvous/channels/index.js", + "matrix-sdk/rendezvous/channels/MSC3903ECDHv2RendezvousChannel.js", +] + +EXTRA_JS_MODULES.matrix.matrix_sdk.rendezvous.transports += [ + "matrix-sdk/rendezvous/transports/index.js", + "matrix-sdk/rendezvous/transports/MSC3886SimpleHttpRendezvousTransport.js", +] + +EXTRA_JS_MODULES.matrix.matrix_sdk.rust_crypto += [ + "matrix-sdk/rust-crypto/browserify-index.js", + "matrix-sdk/rust-crypto/constants.js", + "matrix-sdk/rust-crypto/index.js", + "matrix-sdk/rust-crypto/KeyClaimManager.js", + "matrix-sdk/rust-crypto/OutgoingRequestProcessor.js", + "matrix-sdk/rust-crypto/RoomEncryptor.js", + "matrix-sdk/rust-crypto/rust-crypto.js", +] + EXTRA_JS_MODULES.matrix.matrix_sdk.store += [ "matrix-sdk/store/indexeddb-local-backend.js", "matrix-sdk/store/indexeddb-remote-backend.js", @@ -124,18 +177,23 @@ "matrix-sdk/@types/extensible_events.js", "matrix-sdk/@types/location.js", "matrix-sdk/@types/partials.js", + "matrix-sdk/@types/polls.js", "matrix-sdk/@types/PushRules.js", "matrix-sdk/@types/read_receipts.js", "matrix-sdk/@types/search.js", + "matrix-sdk/@types/sync.js", "matrix-sdk/@types/threepids.js", "matrix-sdk/@types/topic.js", ] EXTRA_JS_MODULES.matrix.matrix_sdk.webrtc += [ + "matrix-sdk/webrtc/audioContext.js", "matrix-sdk/webrtc/call.js", "matrix-sdk/webrtc/callEventHandler.js", "matrix-sdk/webrtc/callEventTypes.js", "matrix-sdk/webrtc/callFeed.js", + "matrix-sdk/webrtc/groupCall.js", + "matrix-sdk/webrtc/groupCallEventHandler.js", "matrix-sdk/webrtc/mediaHandler.js", ] @@ -146,17 +204,12 @@ EXTRA_JS_MODULES.matrix += [ "another-json/another-json.js", "events/events.js", - "qs/dist/qs.js", ] EXTRA_JS_MODULES.matrix.base_x += [ "base-x/index.js", ] -EXTRA_JS_MODULES.matrix.browser_request += [ - "browser-request/index.js", -] - EXTRA_JS_MODULES.matrix.bs58 += [ "bs58/index.js", ] @@ -223,3 +276,57 @@ "matrix-events-sdk/utility/events.js", "matrix-events-sdk/utility/MessageMatchers.js", ] + +EXTRA_JS_MODULES.matrix.matrix_widget_api += [ + "matrix-widget-api/ClientWidgetApi.js", + "matrix-widget-api/index.js", + "matrix-widget-api/Symbols.js", + "matrix-widget-api/WidgetApi.js", +] + +EXTRA_JS_MODULES.matrix.matrix_widget_api.driver += [ + "matrix-widget-api/driver/WidgetDriver.js", +] + +EXTRA_JS_MODULES.matrix.matrix_widget_api.interfaces += [ + "matrix-widget-api/interfaces/ApiVersion.js", + "matrix-widget-api/interfaces/Capabilities.js", + "matrix-widget-api/interfaces/GetOpenIDAction.js", + "matrix-widget-api/interfaces/IWidgetApiErrorResponse.js", + "matrix-widget-api/interfaces/ModalButtonKind.js", + "matrix-widget-api/interfaces/ModalWidgetActions.js", + "matrix-widget-api/interfaces/WidgetApiAction.js", + "matrix-widget-api/interfaces/WidgetApiDirection.js", + "matrix-widget-api/interfaces/WidgetKind.js", + "matrix-widget-api/interfaces/WidgetType.js", +] + +EXTRA_JS_MODULES.matrix.matrix_widget_api.models += [ + "matrix-widget-api/models/Widget.js", + "matrix-widget-api/models/WidgetEventCapability.js", + "matrix-widget-api/models/WidgetParser.js", +] + +EXTRA_JS_MODULES.matrix.matrix_widget_api.models.validation += [ + "matrix-widget-api/models/validation/url.js", + "matrix-widget-api/models/validation/utils.js", +] + +EXTRA_JS_MODULES.matrix.matrix_widget_api.templating += [ + "matrix-widget-api/templating/url-template.js", +] + +EXTRA_JS_MODULES.matrix.matrix_widget_api.transport += [ + "matrix-widget-api/transport/PostmessageTransport.js", +] + +EXTRA_JS_MODULES.matrix.matrix_widget_api.util += [ + "matrix-widget-api/util/SimpleObservable.js", +] + +EXTRA_JS_MODULES.matrix.sdp_transform += [ + "sdp-transform/grammar.js", + "sdp-transform/index.js", + "sdp-transform/parser.js", + "sdp-transform/writer.js", +] diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/qs/dist/qs.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/qs/dist/qs.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/qs/dist/qs.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/qs/dist/qs.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,2054 +0,0 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Qs = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i -1) { - return val.split(','); - } - - return val; -}; - -// This is what browsers will submit when the ✓ character occurs in an -// application/x-www-form-urlencoded body and the encoding of the page containing -// the form is iso-8859-1, or when the submitted form has an accept-charset -// attribute of iso-8859-1. Presumably also with other charsets that do not contain -// the ✓ character, such as us-ascii. -var isoSentinel = 'utf8=%26%2310003%3B'; // encodeURIComponent('✓') - -// These are the percent-encoded utf-8 octets representing a checkmark, indicating that the request actually is utf-8 encoded. -var charsetSentinel = 'utf8=%E2%9C%93'; // encodeURIComponent('✓') - -var parseValues = function parseQueryStringValues(str, options) { - var obj = {}; - var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str; - var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit; - var parts = cleanStr.split(options.delimiter, limit); - var skipIndex = -1; // Keep track of where the utf8 sentinel was found - var i; - - var charset = options.charset; - if (options.charsetSentinel) { - for (i = 0; i < parts.length; ++i) { - if (parts[i].indexOf('utf8=') === 0) { - if (parts[i] === charsetSentinel) { - charset = 'utf-8'; - } else if (parts[i] === isoSentinel) { - charset = 'iso-8859-1'; - } - skipIndex = i; - i = parts.length; // The eslint settings do not allow break; - } - } - } - - for (i = 0; i < parts.length; ++i) { - if (i === skipIndex) { - continue; - } - var part = parts[i]; - - var bracketEqualsPos = part.indexOf(']='); - var pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1; - - var key, val; - if (pos === -1) { - key = options.decoder(part, defaults.decoder, charset, 'key'); - val = options.strictNullHandling ? null : ''; - } else { - key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key'); - val = utils.maybeMap( - parseArrayValue(part.slice(pos + 1), options), - function (encodedVal) { - return options.decoder(encodedVal, defaults.decoder, charset, 'value'); - } - ); - } - - if (val && options.interpretNumericEntities && charset === 'iso-8859-1') { - val = interpretNumericEntities(val); - } - - if (part.indexOf('[]=') > -1) { - val = isArray(val) ? [val] : val; - } - - if (has.call(obj, key)) { - obj[key] = utils.combine(obj[key], val); - } else { - obj[key] = val; - } - } - - return obj; -}; - -var parseObject = function (chain, val, options, valuesParsed) { - var leaf = valuesParsed ? val : parseArrayValue(val, options); - - for (var i = chain.length - 1; i >= 0; --i) { - var obj; - var root = chain[i]; - - if (root === '[]' && options.parseArrays) { - obj = [].concat(leaf); - } else { - obj = options.plainObjects ? Object.create(null) : {}; - var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root; - var index = parseInt(cleanRoot, 10); - if (!options.parseArrays && cleanRoot === '') { - obj = { 0: leaf }; - } else if ( - !isNaN(index) - && root !== cleanRoot - && String(index) === cleanRoot - && index >= 0 - && (options.parseArrays && index <= options.arrayLimit) - ) { - obj = []; - obj[index] = leaf; - } else if (cleanRoot !== '__proto__') { - obj[cleanRoot] = leaf; - } - } - - leaf = obj; - } - - return leaf; -}; - -var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesParsed) { - if (!givenKey) { - return; - } - - // Transform dot notation to bracket notation - var key = options.allowDots ? givenKey.replace(/\.([^.[]+)/g, '[$1]') : givenKey; - - // The regex chunks - - var brackets = /(\[[^[\]]*])/; - var child = /(\[[^[\]]*])/g; - - // Get the parent - - var segment = options.depth > 0 && brackets.exec(key); - var parent = segment ? key.slice(0, segment.index) : key; - - // Stash the parent if it exists - - var keys = []; - if (parent) { - // If we aren't using plain objects, optionally prefix keys that would overwrite object prototype properties - if (!options.plainObjects && has.call(Object.prototype, parent)) { - if (!options.allowPrototypes) { - return; - } - } - - keys.push(parent); - } - - // Loop through children appending to the array until we hit depth - - var i = 0; - while (options.depth > 0 && (segment = child.exec(key)) !== null && i < options.depth) { - i += 1; - if (!options.plainObjects && has.call(Object.prototype, segment[1].slice(1, -1))) { - if (!options.allowPrototypes) { - return; - } - } - keys.push(segment[1]); - } - - // If there's a remainder, just add whatever is left - - if (segment) { - keys.push('[' + key.slice(segment.index) + ']'); - } - - return parseObject(keys, val, options, valuesParsed); -}; - -var normalizeParseOptions = function normalizeParseOptions(opts) { - if (!opts) { - return defaults; - } - - if (opts.decoder !== null && opts.decoder !== undefined && typeof opts.decoder !== 'function') { - throw new TypeError('Decoder has to be a function.'); - } - - if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') { - throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'); - } - var charset = typeof opts.charset === 'undefined' ? defaults.charset : opts.charset; - - return { - allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots, - allowPrototypes: typeof opts.allowPrototypes === 'boolean' ? opts.allowPrototypes : defaults.allowPrototypes, - allowSparse: typeof opts.allowSparse === 'boolean' ? opts.allowSparse : defaults.allowSparse, - arrayLimit: typeof opts.arrayLimit === 'number' ? opts.arrayLimit : defaults.arrayLimit, - charset: charset, - charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, - comma: typeof opts.comma === 'boolean' ? opts.comma : defaults.comma, - decoder: typeof opts.decoder === 'function' ? opts.decoder : defaults.decoder, - delimiter: typeof opts.delimiter === 'string' || utils.isRegExp(opts.delimiter) ? opts.delimiter : defaults.delimiter, - // eslint-disable-next-line no-implicit-coercion, no-extra-parens - depth: (typeof opts.depth === 'number' || opts.depth === false) ? +opts.depth : defaults.depth, - ignoreQueryPrefix: opts.ignoreQueryPrefix === true, - interpretNumericEntities: typeof opts.interpretNumericEntities === 'boolean' ? opts.interpretNumericEntities : defaults.interpretNumericEntities, - parameterLimit: typeof opts.parameterLimit === 'number' ? opts.parameterLimit : defaults.parameterLimit, - parseArrays: opts.parseArrays !== false, - plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects, - strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling - }; -}; - -module.exports = function (str, opts) { - var options = normalizeParseOptions(opts); - - if (str === '' || str === null || typeof str === 'undefined') { - return options.plainObjects ? Object.create(null) : {}; - } - - var tempObj = typeof str === 'string' ? parseValues(str, options) : str; - var obj = options.plainObjects ? Object.create(null) : {}; - - // Iterate over the keys and setup the new object - - var keys = Object.keys(tempObj); - for (var i = 0; i < keys.length; ++i) { - var key = keys[i]; - var newObj = parseKeys(key, tempObj[key], options, typeof str === 'string'); - obj = utils.merge(obj, newObj, options); - } - - if (options.allowSparse === true) { - return obj; - } - - return utils.compact(obj); -}; - -},{"./utils":5}],4:[function(require,module,exports){ -'use strict'; - -var getSideChannel = require('side-channel'); -var utils = require('./utils'); -var formats = require('./formats'); -var has = Object.prototype.hasOwnProperty; - -var arrayPrefixGenerators = { - brackets: function brackets(prefix) { - return prefix + '[]'; - }, - comma: 'comma', - indices: function indices(prefix, key) { - return prefix + '[' + key + ']'; - }, - repeat: function repeat(prefix) { - return prefix; - } -}; - -var isArray = Array.isArray; -var split = String.prototype.split; -var push = Array.prototype.push; -var pushToArray = function (arr, valueOrArray) { - push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]); -}; - -var toISO = Date.prototype.toISOString; - -var defaultFormat = formats['default']; -var defaults = { - addQueryPrefix: false, - allowDots: false, - charset: 'utf-8', - charsetSentinel: false, - delimiter: '&', - encode: true, - encoder: utils.encode, - encodeValuesOnly: false, - format: defaultFormat, - formatter: formats.formatters[defaultFormat], - // deprecated - indices: false, - serializeDate: function serializeDate(date) { - return toISO.call(date); - }, - skipNulls: false, - strictNullHandling: false -}; - -var isNonNullishPrimitive = function isNonNullishPrimitive(v) { - return typeof v === 'string' - || typeof v === 'number' - || typeof v === 'boolean' - || typeof v === 'symbol' - || typeof v === 'bigint'; -}; - -var sentinel = {}; - -var stringify = function stringify( - object, - prefix, - generateArrayPrefix, - commaRoundTrip, - strictNullHandling, - skipNulls, - encoder, - filter, - sort, - allowDots, - serializeDate, - format, - formatter, - encodeValuesOnly, - charset, - sideChannel -) { - var obj = object; - - var tmpSc = sideChannel; - var step = 0; - var findFlag = false; - while ((tmpSc = tmpSc.get(sentinel)) !== void undefined && !findFlag) { - // Where object last appeared in the ref tree - var pos = tmpSc.get(object); - step += 1; - if (typeof pos !== 'undefined') { - if (pos === step) { - throw new RangeError('Cyclic object value'); - } else { - findFlag = true; // Break while - } - } - if (typeof tmpSc.get(sentinel) === 'undefined') { - step = 0; - } - } - - if (typeof filter === 'function') { - obj = filter(prefix, obj); - } else if (obj instanceof Date) { - obj = serializeDate(obj); - } else if (generateArrayPrefix === 'comma' && isArray(obj)) { - obj = utils.maybeMap(obj, function (value) { - if (value instanceof Date) { - return serializeDate(value); - } - return value; - }); - } - - if (obj === null) { - if (strictNullHandling) { - return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key', format) : prefix; - } - - obj = ''; - } - - if (isNonNullishPrimitive(obj) || utils.isBuffer(obj)) { - if (encoder) { - var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder, charset, 'key', format); - if (generateArrayPrefix === 'comma' && encodeValuesOnly) { - var valuesArray = split.call(String(obj), ','); - var valuesJoined = ''; - for (var i = 0; i < valuesArray.length; ++i) { - valuesJoined += (i === 0 ? '' : ',') + formatter(encoder(valuesArray[i], defaults.encoder, charset, 'value', format)); - } - return [formatter(keyValue) + (commaRoundTrip && isArray(obj) && valuesArray.length === 1 ? '[]' : '') + '=' + valuesJoined]; - } - return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder, charset, 'value', format))]; - } - return [formatter(prefix) + '=' + formatter(String(obj))]; - } - - var values = []; - - if (typeof obj === 'undefined') { - return values; - } - - var objKeys; - if (generateArrayPrefix === 'comma' && isArray(obj)) { - // we need to join elements in - objKeys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }]; - } else if (isArray(filter)) { - objKeys = filter; - } else { - var keys = Object.keys(obj); - objKeys = sort ? keys.sort(sort) : keys; - } - - var adjustedPrefix = commaRoundTrip && isArray(obj) && obj.length === 1 ? prefix + '[]' : prefix; - - for (var j = 0; j < objKeys.length; ++j) { - var key = objKeys[j]; - var value = typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key]; - - if (skipNulls && value === null) { - continue; - } - - var keyPrefix = isArray(obj) - ? typeof generateArrayPrefix === 'function' ? generateArrayPrefix(adjustedPrefix, key) : adjustedPrefix - : adjustedPrefix + (allowDots ? '.' + key : '[' + key + ']'); - - sideChannel.set(object, step); - var valueSideChannel = getSideChannel(); - valueSideChannel.set(sentinel, sideChannel); - pushToArray(values, stringify( - value, - keyPrefix, - generateArrayPrefix, - commaRoundTrip, - strictNullHandling, - skipNulls, - encoder, - filter, - sort, - allowDots, - serializeDate, - format, - formatter, - encodeValuesOnly, - charset, - valueSideChannel - )); - } - - return values; -}; - -var normalizeStringifyOptions = function normalizeStringifyOptions(opts) { - if (!opts) { - return defaults; - } - - if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') { - throw new TypeError('Encoder has to be a function.'); - } - - var charset = opts.charset || defaults.charset; - if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') { - throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'); - } - - var format = formats['default']; - if (typeof opts.format !== 'undefined') { - if (!has.call(formats.formatters, opts.format)) { - throw new TypeError('Unknown format option provided.'); - } - format = opts.format; - } - var formatter = formats.formatters[format]; - - var filter = defaults.filter; - if (typeof opts.filter === 'function' || isArray(opts.filter)) { - filter = opts.filter; - } - - return { - addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix, - allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots, - charset: charset, - charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, - delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter, - encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode, - encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder, - encodeValuesOnly: typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly, - filter: filter, - format: format, - formatter: formatter, - serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate, - skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls, - sort: typeof opts.sort === 'function' ? opts.sort : null, - strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling - }; -}; - -module.exports = function (object, opts) { - var obj = object; - var options = normalizeStringifyOptions(opts); - - var objKeys; - var filter; - - if (typeof options.filter === 'function') { - filter = options.filter; - obj = filter('', obj); - } else if (isArray(options.filter)) { - filter = options.filter; - objKeys = filter; - } - - var keys = []; - - if (typeof obj !== 'object' || obj === null) { - return ''; - } - - var arrayFormat; - if (opts && opts.arrayFormat in arrayPrefixGenerators) { - arrayFormat = opts.arrayFormat; - } else if (opts && 'indices' in opts) { - arrayFormat = opts.indices ? 'indices' : 'repeat'; - } else { - arrayFormat = 'indices'; - } - - var generateArrayPrefix = arrayPrefixGenerators[arrayFormat]; - if (opts && 'commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') { - throw new TypeError('`commaRoundTrip` must be a boolean, or absent'); - } - var commaRoundTrip = generateArrayPrefix === 'comma' && opts && opts.commaRoundTrip; - - if (!objKeys) { - objKeys = Object.keys(obj); - } - - if (options.sort) { - objKeys.sort(options.sort); - } - - var sideChannel = getSideChannel(); - for (var i = 0; i < objKeys.length; ++i) { - var key = objKeys[i]; - - if (options.skipNulls && obj[key] === null) { - continue; - } - pushToArray(keys, stringify( - obj[key], - key, - generateArrayPrefix, - commaRoundTrip, - options.strictNullHandling, - options.skipNulls, - options.encode ? options.encoder : null, - options.filter, - options.sort, - options.allowDots, - options.serializeDate, - options.format, - options.formatter, - options.encodeValuesOnly, - options.charset, - sideChannel - )); - } - - var joined = keys.join(options.delimiter); - var prefix = options.addQueryPrefix === true ? '?' : ''; - - if (options.charsetSentinel) { - if (options.charset === 'iso-8859-1') { - // encodeURIComponent('✓'), the "numeric entity" representation of a checkmark - prefix += 'utf8=%26%2310003%3B&'; - } else { - // encodeURIComponent('✓') - prefix += 'utf8=%E2%9C%93&'; - } - } - - return joined.length > 0 ? prefix + joined : ''; -}; - -},{"./formats":1,"./utils":5,"side-channel":16}],5:[function(require,module,exports){ -'use strict'; - -var formats = require('./formats'); - -var has = Object.prototype.hasOwnProperty; -var isArray = Array.isArray; - -var hexTable = (function () { - var array = []; - for (var i = 0; i < 256; ++i) { - array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase()); - } - - return array; -}()); - -var compactQueue = function compactQueue(queue) { - while (queue.length > 1) { - var item = queue.pop(); - var obj = item.obj[item.prop]; - - if (isArray(obj)) { - var compacted = []; - - for (var j = 0; j < obj.length; ++j) { - if (typeof obj[j] !== 'undefined') { - compacted.push(obj[j]); - } - } - - item.obj[item.prop] = compacted; - } - } -}; - -var arrayToObject = function arrayToObject(source, options) { - var obj = options && options.plainObjects ? Object.create(null) : {}; - for (var i = 0; i < source.length; ++i) { - if (typeof source[i] !== 'undefined') { - obj[i] = source[i]; - } - } - - return obj; -}; - -var merge = function merge(target, source, options) { - /* eslint no-param-reassign: 0 */ - if (!source) { - return target; - } - - if (typeof source !== 'object') { - if (isArray(target)) { - target.push(source); - } else if (target && typeof target === 'object') { - if ((options && (options.plainObjects || options.allowPrototypes)) || !has.call(Object.prototype, source)) { - target[source] = true; - } - } else { - return [target, source]; - } - - return target; - } - - if (!target || typeof target !== 'object') { - return [target].concat(source); - } - - var mergeTarget = target; - if (isArray(target) && !isArray(source)) { - mergeTarget = arrayToObject(target, options); - } - - if (isArray(target) && isArray(source)) { - source.forEach(function (item, i) { - if (has.call(target, i)) { - var targetItem = target[i]; - if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') { - target[i] = merge(targetItem, item, options); - } else { - target.push(item); - } - } else { - target[i] = item; - } - }); - return target; - } - - return Object.keys(source).reduce(function (acc, key) { - var value = source[key]; - - if (has.call(acc, key)) { - acc[key] = merge(acc[key], value, options); - } else { - acc[key] = value; - } - return acc; - }, mergeTarget); -}; - -var assign = function assignSingleSource(target, source) { - return Object.keys(source).reduce(function (acc, key) { - acc[key] = source[key]; - return acc; - }, target); -}; - -var decode = function (str, decoder, charset) { - var strWithoutPlus = str.replace(/\+/g, ' '); - if (charset === 'iso-8859-1') { - // unescape never throws, no try...catch needed: - return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape); - } - // utf-8 - try { - return decodeURIComponent(strWithoutPlus); - } catch (e) { - return strWithoutPlus; - } -}; - -var encode = function encode(str, defaultEncoder, charset, kind, format) { - // This code was originally written by Brian White (mscdex) for the io.js core querystring library. - // It has been adapted here for stricter adherence to RFC 3986 - if (str.length === 0) { - return str; - } - - var string = str; - if (typeof str === 'symbol') { - string = Symbol.prototype.toString.call(str); - } else if (typeof str !== 'string') { - string = String(str); - } - - if (charset === 'iso-8859-1') { - return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) { - return '%26%23' + parseInt($0.slice(2), 16) + '%3B'; - }); - } - - var out = ''; - for (var i = 0; i < string.length; ++i) { - var c = string.charCodeAt(i); - - if ( - c === 0x2D // - - || c === 0x2E // . - || c === 0x5F // _ - || c === 0x7E // ~ - || (c >= 0x30 && c <= 0x39) // 0-9 - || (c >= 0x41 && c <= 0x5A) // a-z - || (c >= 0x61 && c <= 0x7A) // A-Z - || (format === formats.RFC1738 && (c === 0x28 || c === 0x29)) // ( ) - ) { - out += string.charAt(i); - continue; - } - - if (c < 0x80) { - out = out + hexTable[c]; - continue; - } - - if (c < 0x800) { - out = out + (hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]); - continue; - } - - if (c < 0xD800 || c >= 0xE000) { - out = out + (hexTable[0xE0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]); - continue; - } - - i += 1; - c = 0x10000 + (((c & 0x3FF) << 10) | (string.charCodeAt(i) & 0x3FF)); - /* eslint operator-linebreak: [2, "before"] */ - out += hexTable[0xF0 | (c >> 18)] - + hexTable[0x80 | ((c >> 12) & 0x3F)] - + hexTable[0x80 | ((c >> 6) & 0x3F)] - + hexTable[0x80 | (c & 0x3F)]; - } - - return out; -}; - -var compact = function compact(value) { - var queue = [{ obj: { o: value }, prop: 'o' }]; - var refs = []; - - for (var i = 0; i < queue.length; ++i) { - var item = queue[i]; - var obj = item.obj[item.prop]; - - var keys = Object.keys(obj); - for (var j = 0; j < keys.length; ++j) { - var key = keys[j]; - var val = obj[key]; - if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) { - queue.push({ obj: obj, prop: key }); - refs.push(val); - } - } - } - - compactQueue(queue); - - return value; -}; - -var isRegExp = function isRegExp(obj) { - return Object.prototype.toString.call(obj) === '[object RegExp]'; -}; - -var isBuffer = function isBuffer(obj) { - if (!obj || typeof obj !== 'object') { - return false; - } - - return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); -}; - -var combine = function combine(a, b) { - return [].concat(a, b); -}; - -var maybeMap = function maybeMap(val, fn) { - if (isArray(val)) { - var mapped = []; - for (var i = 0; i < val.length; i += 1) { - mapped.push(fn(val[i])); - } - return mapped; - } - return fn(val); -}; - -module.exports = { - arrayToObject: arrayToObject, - assign: assign, - combine: combine, - compact: compact, - decode: decode, - encode: encode, - isBuffer: isBuffer, - isRegExp: isRegExp, - maybeMap: maybeMap, - merge: merge -}; - -},{"./formats":1}],6:[function(require,module,exports){ - -},{}],7:[function(require,module,exports){ -'use strict'; - -var GetIntrinsic = require('get-intrinsic'); - -var callBind = require('./'); - -var $indexOf = callBind(GetIntrinsic('String.prototype.indexOf')); - -module.exports = function callBoundIntrinsic(name, allowMissing) { - var intrinsic = GetIntrinsic(name, !!allowMissing); - if (typeof intrinsic === 'function' && $indexOf(name, '.prototype.') > -1) { - return callBind(intrinsic); - } - return intrinsic; -}; - -},{"./":8,"get-intrinsic":11}],8:[function(require,module,exports){ -'use strict'; - -var bind = require('function-bind'); -var GetIntrinsic = require('get-intrinsic'); - -var $apply = GetIntrinsic('%Function.prototype.apply%'); -var $call = GetIntrinsic('%Function.prototype.call%'); -var $reflectApply = GetIntrinsic('%Reflect.apply%', true) || bind.call($call, $apply); - -var $gOPD = GetIntrinsic('%Object.getOwnPropertyDescriptor%', true); -var $defineProperty = GetIntrinsic('%Object.defineProperty%', true); -var $max = GetIntrinsic('%Math.max%'); - -if ($defineProperty) { - try { - $defineProperty({}, 'a', { value: 1 }); - } catch (e) { - // IE 8 has a broken defineProperty - $defineProperty = null; - } -} - -module.exports = function callBind(originalFunction) { - var func = $reflectApply(bind, $call, arguments); - if ($gOPD && $defineProperty) { - var desc = $gOPD(func, 'length'); - if (desc.configurable) { - // original length, plus the receiver, minus any additional arguments (after the receiver) - $defineProperty( - func, - 'length', - { value: 1 + $max(0, originalFunction.length - (arguments.length - 1)) } - ); - } - } - return func; -}; - -var applyBind = function applyBind() { - return $reflectApply(bind, $apply, arguments); -}; - -if ($defineProperty) { - $defineProperty(module.exports, 'apply', { value: applyBind }); -} else { - module.exports.apply = applyBind; -} - -},{"function-bind":10,"get-intrinsic":11}],9:[function(require,module,exports){ -'use strict'; - -/* eslint no-invalid-this: 1 */ - -var ERROR_MESSAGE = 'Function.prototype.bind called on incompatible '; -var slice = Array.prototype.slice; -var toStr = Object.prototype.toString; -var funcType = '[object Function]'; - -module.exports = function bind(that) { - var target = this; - if (typeof target !== 'function' || toStr.call(target) !== funcType) { - throw new TypeError(ERROR_MESSAGE + target); - } - var args = slice.call(arguments, 1); - - var bound; - var binder = function () { - if (this instanceof bound) { - var result = target.apply( - this, - args.concat(slice.call(arguments)) - ); - if (Object(result) === result) { - return result; - } - return this; - } else { - return target.apply( - that, - args.concat(slice.call(arguments)) - ); - } - }; - - var boundLength = Math.max(0, target.length - args.length); - var boundArgs = []; - for (var i = 0; i < boundLength; i++) { - boundArgs.push('$' + i); - } - - bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder); - - if (target.prototype) { - var Empty = function Empty() {}; - Empty.prototype = target.prototype; - bound.prototype = new Empty(); - Empty.prototype = null; - } - - return bound; -}; - -},{}],10:[function(require,module,exports){ -'use strict'; - -var implementation = require('./implementation'); - -module.exports = Function.prototype.bind || implementation; - -},{"./implementation":9}],11:[function(require,module,exports){ -'use strict'; - -var undefined; - -var $SyntaxError = SyntaxError; -var $Function = Function; -var $TypeError = TypeError; - -// eslint-disable-next-line consistent-return -var getEvalledConstructor = function (expressionSyntax) { - try { - return $Function('"use strict"; return (' + expressionSyntax + ').constructor;')(); - } catch (e) {} -}; - -var $gOPD = Object.getOwnPropertyDescriptor; -if ($gOPD) { - try { - $gOPD({}, ''); - } catch (e) { - $gOPD = null; // this is IE 8, which has a broken gOPD - } -} - -var throwTypeError = function () { - throw new $TypeError(); -}; -var ThrowTypeError = $gOPD - ? (function () { - try { - // eslint-disable-next-line no-unused-expressions, no-caller, no-restricted-properties - arguments.callee; // IE 8 does not throw here - return throwTypeError; - } catch (calleeThrows) { - try { - // IE 8 throws on Object.getOwnPropertyDescriptor(arguments, '') - return $gOPD(arguments, 'callee').get; - } catch (gOPDthrows) { - return throwTypeError; - } - } - }()) - : throwTypeError; - -var hasSymbols = require('has-symbols')(); - -var getProto = Object.getPrototypeOf || function (x) { return x.__proto__; }; // eslint-disable-line no-proto - -var needsEval = {}; - -var TypedArray = typeof Uint8Array === 'undefined' ? undefined : getProto(Uint8Array); - -var INTRINSICS = { - '%AggregateError%': typeof AggregateError === 'undefined' ? undefined : AggregateError, - '%Array%': Array, - '%ArrayBuffer%': typeof ArrayBuffer === 'undefined' ? undefined : ArrayBuffer, - '%ArrayIteratorPrototype%': hasSymbols ? getProto([][Symbol.iterator]()) : undefined, - '%AsyncFromSyncIteratorPrototype%': undefined, - '%AsyncFunction%': needsEval, - '%AsyncGenerator%': needsEval, - '%AsyncGeneratorFunction%': needsEval, - '%AsyncIteratorPrototype%': needsEval, - '%Atomics%': typeof Atomics === 'undefined' ? undefined : Atomics, - '%BigInt%': typeof BigInt === 'undefined' ? undefined : BigInt, - '%Boolean%': Boolean, - '%DataView%': typeof DataView === 'undefined' ? undefined : DataView, - '%Date%': Date, - '%decodeURI%': decodeURI, - '%decodeURIComponent%': decodeURIComponent, - '%encodeURI%': encodeURI, - '%encodeURIComponent%': encodeURIComponent, - '%Error%': Error, - '%eval%': eval, // eslint-disable-line no-eval - '%EvalError%': EvalError, - '%Float32Array%': typeof Float32Array === 'undefined' ? undefined : Float32Array, - '%Float64Array%': typeof Float64Array === 'undefined' ? undefined : Float64Array, - '%FinalizationRegistry%': typeof FinalizationRegistry === 'undefined' ? undefined : FinalizationRegistry, - '%Function%': $Function, - '%GeneratorFunction%': needsEval, - '%Int8Array%': typeof Int8Array === 'undefined' ? undefined : Int8Array, - '%Int16Array%': typeof Int16Array === 'undefined' ? undefined : Int16Array, - '%Int32Array%': typeof Int32Array === 'undefined' ? undefined : Int32Array, - '%isFinite%': isFinite, - '%isNaN%': isNaN, - '%IteratorPrototype%': hasSymbols ? getProto(getProto([][Symbol.iterator]())) : undefined, - '%JSON%': typeof JSON === 'object' ? JSON : undefined, - '%Map%': typeof Map === 'undefined' ? undefined : Map, - '%MapIteratorPrototype%': typeof Map === 'undefined' || !hasSymbols ? undefined : getProto(new Map()[Symbol.iterator]()), - '%Math%': Math, - '%Number%': Number, - '%Object%': Object, - '%parseFloat%': parseFloat, - '%parseInt%': parseInt, - '%Promise%': typeof Promise === 'undefined' ? undefined : Promise, - '%Proxy%': typeof Proxy === 'undefined' ? undefined : Proxy, - '%RangeError%': RangeError, - '%ReferenceError%': ReferenceError, - '%Reflect%': typeof Reflect === 'undefined' ? undefined : Reflect, - '%RegExp%': RegExp, - '%Set%': typeof Set === 'undefined' ? undefined : Set, - '%SetIteratorPrototype%': typeof Set === 'undefined' || !hasSymbols ? undefined : getProto(new Set()[Symbol.iterator]()), - '%SharedArrayBuffer%': typeof SharedArrayBuffer === 'undefined' ? undefined : SharedArrayBuffer, - '%String%': String, - '%StringIteratorPrototype%': hasSymbols ? getProto(''[Symbol.iterator]()) : undefined, - '%Symbol%': hasSymbols ? Symbol : undefined, - '%SyntaxError%': $SyntaxError, - '%ThrowTypeError%': ThrowTypeError, - '%TypedArray%': TypedArray, - '%TypeError%': $TypeError, - '%Uint8Array%': typeof Uint8Array === 'undefined' ? undefined : Uint8Array, - '%Uint8ClampedArray%': typeof Uint8ClampedArray === 'undefined' ? undefined : Uint8ClampedArray, - '%Uint16Array%': typeof Uint16Array === 'undefined' ? undefined : Uint16Array, - '%Uint32Array%': typeof Uint32Array === 'undefined' ? undefined : Uint32Array, - '%URIError%': URIError, - '%WeakMap%': typeof WeakMap === 'undefined' ? undefined : WeakMap, - '%WeakRef%': typeof WeakRef === 'undefined' ? undefined : WeakRef, - '%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet -}; - -var doEval = function doEval(name) { - var value; - if (name === '%AsyncFunction%') { - value = getEvalledConstructor('async function () {}'); - } else if (name === '%GeneratorFunction%') { - value = getEvalledConstructor('function* () {}'); - } else if (name === '%AsyncGeneratorFunction%') { - value = getEvalledConstructor('async function* () {}'); - } else if (name === '%AsyncGenerator%') { - var fn = doEval('%AsyncGeneratorFunction%'); - if (fn) { - value = fn.prototype; - } - } else if (name === '%AsyncIteratorPrototype%') { - var gen = doEval('%AsyncGenerator%'); - if (gen) { - value = getProto(gen.prototype); - } - } - - INTRINSICS[name] = value; - - return value; -}; - -var LEGACY_ALIASES = { - '%ArrayBufferPrototype%': ['ArrayBuffer', 'prototype'], - '%ArrayPrototype%': ['Array', 'prototype'], - '%ArrayProto_entries%': ['Array', 'prototype', 'entries'], - '%ArrayProto_forEach%': ['Array', 'prototype', 'forEach'], - '%ArrayProto_keys%': ['Array', 'prototype', 'keys'], - '%ArrayProto_values%': ['Array', 'prototype', 'values'], - '%AsyncFunctionPrototype%': ['AsyncFunction', 'prototype'], - '%AsyncGenerator%': ['AsyncGeneratorFunction', 'prototype'], - '%AsyncGeneratorPrototype%': ['AsyncGeneratorFunction', 'prototype', 'prototype'], - '%BooleanPrototype%': ['Boolean', 'prototype'], - '%DataViewPrototype%': ['DataView', 'prototype'], - '%DatePrototype%': ['Date', 'prototype'], - '%ErrorPrototype%': ['Error', 'prototype'], - '%EvalErrorPrototype%': ['EvalError', 'prototype'], - '%Float32ArrayPrototype%': ['Float32Array', 'prototype'], - '%Float64ArrayPrototype%': ['Float64Array', 'prototype'], - '%FunctionPrototype%': ['Function', 'prototype'], - '%Generator%': ['GeneratorFunction', 'prototype'], - '%GeneratorPrototype%': ['GeneratorFunction', 'prototype', 'prototype'], - '%Int8ArrayPrototype%': ['Int8Array', 'prototype'], - '%Int16ArrayPrototype%': ['Int16Array', 'prototype'], - '%Int32ArrayPrototype%': ['Int32Array', 'prototype'], - '%JSONParse%': ['JSON', 'parse'], - '%JSONStringify%': ['JSON', 'stringify'], - '%MapPrototype%': ['Map', 'prototype'], - '%NumberPrototype%': ['Number', 'prototype'], - '%ObjectPrototype%': ['Object', 'prototype'], - '%ObjProto_toString%': ['Object', 'prototype', 'toString'], - '%ObjProto_valueOf%': ['Object', 'prototype', 'valueOf'], - '%PromisePrototype%': ['Promise', 'prototype'], - '%PromiseProto_then%': ['Promise', 'prototype', 'then'], - '%Promise_all%': ['Promise', 'all'], - '%Promise_reject%': ['Promise', 'reject'], - '%Promise_resolve%': ['Promise', 'resolve'], - '%RangeErrorPrototype%': ['RangeError', 'prototype'], - '%ReferenceErrorPrototype%': ['ReferenceError', 'prototype'], - '%RegExpPrototype%': ['RegExp', 'prototype'], - '%SetPrototype%': ['Set', 'prototype'], - '%SharedArrayBufferPrototype%': ['SharedArrayBuffer', 'prototype'], - '%StringPrototype%': ['String', 'prototype'], - '%SymbolPrototype%': ['Symbol', 'prototype'], - '%SyntaxErrorPrototype%': ['SyntaxError', 'prototype'], - '%TypedArrayPrototype%': ['TypedArray', 'prototype'], - '%TypeErrorPrototype%': ['TypeError', 'prototype'], - '%Uint8ArrayPrototype%': ['Uint8Array', 'prototype'], - '%Uint8ClampedArrayPrototype%': ['Uint8ClampedArray', 'prototype'], - '%Uint16ArrayPrototype%': ['Uint16Array', 'prototype'], - '%Uint32ArrayPrototype%': ['Uint32Array', 'prototype'], - '%URIErrorPrototype%': ['URIError', 'prototype'], - '%WeakMapPrototype%': ['WeakMap', 'prototype'], - '%WeakSetPrototype%': ['WeakSet', 'prototype'] -}; - -var bind = require('function-bind'); -var hasOwn = require('has'); -var $concat = bind.call(Function.call, Array.prototype.concat); -var $spliceApply = bind.call(Function.apply, Array.prototype.splice); -var $replace = bind.call(Function.call, String.prototype.replace); -var $strSlice = bind.call(Function.call, String.prototype.slice); - -/* adapted from https://github.com/lodash/lodash/blob/4.17.15/dist/lodash.js#L6735-L6744 */ -var rePropName = /[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g; -var reEscapeChar = /\\(\\)?/g; /** Used to match backslashes in property paths. */ -var stringToPath = function stringToPath(string) { - var first = $strSlice(string, 0, 1); - var last = $strSlice(string, -1); - if (first === '%' && last !== '%') { - throw new $SyntaxError('invalid intrinsic syntax, expected closing `%`'); - } else if (last === '%' && first !== '%') { - throw new $SyntaxError('invalid intrinsic syntax, expected opening `%`'); - } - var result = []; - $replace(string, rePropName, function (match, number, quote, subString) { - result[result.length] = quote ? $replace(subString, reEscapeChar, '$1') : number || match; - }); - return result; -}; -/* end adaptation */ - -var getBaseIntrinsic = function getBaseIntrinsic(name, allowMissing) { - var intrinsicName = name; - var alias; - if (hasOwn(LEGACY_ALIASES, intrinsicName)) { - alias = LEGACY_ALIASES[intrinsicName]; - intrinsicName = '%' + alias[0] + '%'; - } - - if (hasOwn(INTRINSICS, intrinsicName)) { - var value = INTRINSICS[intrinsicName]; - if (value === needsEval) { - value = doEval(intrinsicName); - } - if (typeof value === 'undefined' && !allowMissing) { - throw new $TypeError('intrinsic ' + name + ' exists, but is not available. Please file an issue!'); - } - - return { - alias: alias, - name: intrinsicName, - value: value - }; - } - - throw new $SyntaxError('intrinsic ' + name + ' does not exist!'); -}; - -module.exports = function GetIntrinsic(name, allowMissing) { - if (typeof name !== 'string' || name.length === 0) { - throw new $TypeError('intrinsic name must be a non-empty string'); - } - if (arguments.length > 1 && typeof allowMissing !== 'boolean') { - throw new $TypeError('"allowMissing" argument must be a boolean'); - } - - var parts = stringToPath(name); - var intrinsicBaseName = parts.length > 0 ? parts[0] : ''; - - var intrinsic = getBaseIntrinsic('%' + intrinsicBaseName + '%', allowMissing); - var intrinsicRealName = intrinsic.name; - var value = intrinsic.value; - var skipFurtherCaching = false; - - var alias = intrinsic.alias; - if (alias) { - intrinsicBaseName = alias[0]; - $spliceApply(parts, $concat([0, 1], alias)); - } - - for (var i = 1, isOwn = true; i < parts.length; i += 1) { - var part = parts[i]; - var first = $strSlice(part, 0, 1); - var last = $strSlice(part, -1); - if ( - ( - (first === '"' || first === "'" || first === '`') - || (last === '"' || last === "'" || last === '`') - ) - && first !== last - ) { - throw new $SyntaxError('property names with quotes must have matching quotes'); - } - if (part === 'constructor' || !isOwn) { - skipFurtherCaching = true; - } - - intrinsicBaseName += '.' + part; - intrinsicRealName = '%' + intrinsicBaseName + '%'; - - if (hasOwn(INTRINSICS, intrinsicRealName)) { - value = INTRINSICS[intrinsicRealName]; - } else if (value != null) { - if (!(part in value)) { - if (!allowMissing) { - throw new $TypeError('base intrinsic for ' + name + ' exists, but the property is not available.'); - } - return void undefined; - } - if ($gOPD && (i + 1) >= parts.length) { - var desc = $gOPD(value, part); - isOwn = !!desc; - - // By convention, when a data property is converted to an accessor - // property to emulate a data property that does not suffer from - // the override mistake, that accessor's getter is marked with - // an `originalValue` property. Here, when we detect this, we - // uphold the illusion by pretending to see that original data - // property, i.e., returning the value rather than the getter - // itself. - if (isOwn && 'get' in desc && !('originalValue' in desc.get)) { - value = desc.get; - } else { - value = value[part]; - } - } else { - isOwn = hasOwn(value, part); - value = value[part]; - } - - if (isOwn && !skipFurtherCaching) { - INTRINSICS[intrinsicRealName] = value; - } - } - } - return value; -}; - -},{"function-bind":10,"has":14,"has-symbols":12}],12:[function(require,module,exports){ -'use strict'; - -var origSymbol = typeof Symbol !== 'undefined' && Symbol; -var hasSymbolSham = require('./shams'); - -module.exports = function hasNativeSymbols() { - if (typeof origSymbol !== 'function') { return false; } - if (typeof Symbol !== 'function') { return false; } - if (typeof origSymbol('foo') !== 'symbol') { return false; } - if (typeof Symbol('bar') !== 'symbol') { return false; } - - return hasSymbolSham(); -}; - -},{"./shams":13}],13:[function(require,module,exports){ -'use strict'; - -/* eslint complexity: [2, 18], max-statements: [2, 33] */ -module.exports = function hasSymbols() { - if (typeof Symbol !== 'function' || typeof Object.getOwnPropertySymbols !== 'function') { return false; } - if (typeof Symbol.iterator === 'symbol') { return true; } - - var obj = {}; - var sym = Symbol('test'); - var symObj = Object(sym); - if (typeof sym === 'string') { return false; } - - if (Object.prototype.toString.call(sym) !== '[object Symbol]') { return false; } - if (Object.prototype.toString.call(symObj) !== '[object Symbol]') { return false; } - - // temp disabled per https://github.com/ljharb/object.assign/issues/17 - // if (sym instanceof Symbol) { return false; } - // temp disabled per https://github.com/WebReflection/get-own-property-symbols/issues/4 - // if (!(symObj instanceof Symbol)) { return false; } - - // if (typeof Symbol.prototype.toString !== 'function') { return false; } - // if (String(sym) !== Symbol.prototype.toString.call(sym)) { return false; } - - var symVal = 42; - obj[sym] = symVal; - for (sym in obj) { return false; } // eslint-disable-line no-restricted-syntax, no-unreachable-loop - if (typeof Object.keys === 'function' && Object.keys(obj).length !== 0) { return false; } - - if (typeof Object.getOwnPropertyNames === 'function' && Object.getOwnPropertyNames(obj).length !== 0) { return false; } - - var syms = Object.getOwnPropertySymbols(obj); - if (syms.length !== 1 || syms[0] !== sym) { return false; } - - if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { return false; } - - if (typeof Object.getOwnPropertyDescriptor === 'function') { - var descriptor = Object.getOwnPropertyDescriptor(obj, sym); - if (descriptor.value !== symVal || descriptor.enumerable !== true) { return false; } - } - - return true; -}; - -},{}],14:[function(require,module,exports){ -'use strict'; - -var bind = require('function-bind'); - -module.exports = bind.call(Function.call, Object.prototype.hasOwnProperty); - -},{"function-bind":10}],15:[function(require,module,exports){ -var hasMap = typeof Map === 'function' && Map.prototype; -var mapSizeDescriptor = Object.getOwnPropertyDescriptor && hasMap ? Object.getOwnPropertyDescriptor(Map.prototype, 'size') : null; -var mapSize = hasMap && mapSizeDescriptor && typeof mapSizeDescriptor.get === 'function' ? mapSizeDescriptor.get : null; -var mapForEach = hasMap && Map.prototype.forEach; -var hasSet = typeof Set === 'function' && Set.prototype; -var setSizeDescriptor = Object.getOwnPropertyDescriptor && hasSet ? Object.getOwnPropertyDescriptor(Set.prototype, 'size') : null; -var setSize = hasSet && setSizeDescriptor && typeof setSizeDescriptor.get === 'function' ? setSizeDescriptor.get : null; -var setForEach = hasSet && Set.prototype.forEach; -var hasWeakMap = typeof WeakMap === 'function' && WeakMap.prototype; -var weakMapHas = hasWeakMap ? WeakMap.prototype.has : null; -var hasWeakSet = typeof WeakSet === 'function' && WeakSet.prototype; -var weakSetHas = hasWeakSet ? WeakSet.prototype.has : null; -var hasWeakRef = typeof WeakRef === 'function' && WeakRef.prototype; -var weakRefDeref = hasWeakRef ? WeakRef.prototype.deref : null; -var booleanValueOf = Boolean.prototype.valueOf; -var objectToString = Object.prototype.toString; -var functionToString = Function.prototype.toString; -var $match = String.prototype.match; -var $slice = String.prototype.slice; -var $replace = String.prototype.replace; -var $toUpperCase = String.prototype.toUpperCase; -var $toLowerCase = String.prototype.toLowerCase; -var $test = RegExp.prototype.test; -var $concat = Array.prototype.concat; -var $join = Array.prototype.join; -var $arrSlice = Array.prototype.slice; -var $floor = Math.floor; -var bigIntValueOf = typeof BigInt === 'function' ? BigInt.prototype.valueOf : null; -var gOPS = Object.getOwnPropertySymbols; -var symToString = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' ? Symbol.prototype.toString : null; -var hasShammedSymbols = typeof Symbol === 'function' && typeof Symbol.iterator === 'object'; -// ie, `has-tostringtag/shams -var toStringTag = typeof Symbol === 'function' && Symbol.toStringTag && (typeof Symbol.toStringTag === hasShammedSymbols ? 'object' : 'symbol') - ? Symbol.toStringTag - : null; -var isEnumerable = Object.prototype.propertyIsEnumerable; - -var gPO = (typeof Reflect === 'function' ? Reflect.getPrototypeOf : Object.getPrototypeOf) || ( - [].__proto__ === Array.prototype // eslint-disable-line no-proto - ? function (O) { - return O.__proto__; // eslint-disable-line no-proto - } - : null -); - -function addNumericSeparator(num, str) { - if ( - num === Infinity - || num === -Infinity - || num !== num - || (num && num > -1000 && num < 1000) - || $test.call(/e/, str) - ) { - return str; - } - var sepRegex = /[0-9](?=(?:[0-9]{3})+(?![0-9]))/g; - if (typeof num === 'number') { - var int = num < 0 ? -$floor(-num) : $floor(num); // trunc(num) - if (int !== num) { - var intStr = String(int); - var dec = $slice.call(str, intStr.length + 1); - return $replace.call(intStr, sepRegex, '$&_') + '.' + $replace.call($replace.call(dec, /([0-9]{3})/g, '$&_'), /_$/, ''); - } - } - return $replace.call(str, sepRegex, '$&_'); -} - -var utilInspect = require('./util.inspect'); -var inspectCustom = utilInspect.custom; -var inspectSymbol = isSymbol(inspectCustom) ? inspectCustom : null; - -module.exports = function inspect_(obj, options, depth, seen) { - var opts = options || {}; - - if (has(opts, 'quoteStyle') && (opts.quoteStyle !== 'single' && opts.quoteStyle !== 'double')) { - throw new TypeError('option "quoteStyle" must be "single" or "double"'); - } - if ( - has(opts, 'maxStringLength') && (typeof opts.maxStringLength === 'number' - ? opts.maxStringLength < 0 && opts.maxStringLength !== Infinity - : opts.maxStringLength !== null - ) - ) { - throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`'); - } - var customInspect = has(opts, 'customInspect') ? opts.customInspect : true; - if (typeof customInspect !== 'boolean' && customInspect !== 'symbol') { - throw new TypeError('option "customInspect", if provided, must be `true`, `false`, or `\'symbol\'`'); - } - - if ( - has(opts, 'indent') - && opts.indent !== null - && opts.indent !== '\t' - && !(parseInt(opts.indent, 10) === opts.indent && opts.indent > 0) - ) { - throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`'); - } - if (has(opts, 'numericSeparator') && typeof opts.numericSeparator !== 'boolean') { - throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`'); - } - var numericSeparator = opts.numericSeparator; - - if (typeof obj === 'undefined') { - return 'undefined'; - } - if (obj === null) { - return 'null'; - } - if (typeof obj === 'boolean') { - return obj ? 'true' : 'false'; - } - - if (typeof obj === 'string') { - return inspectString(obj, opts); - } - if (typeof obj === 'number') { - if (obj === 0) { - return Infinity / obj > 0 ? '0' : '-0'; - } - var str = String(obj); - return numericSeparator ? addNumericSeparator(obj, str) : str; - } - if (typeof obj === 'bigint') { - var bigIntStr = String(obj) + 'n'; - return numericSeparator ? addNumericSeparator(obj, bigIntStr) : bigIntStr; - } - - var maxDepth = typeof opts.depth === 'undefined' ? 5 : opts.depth; - if (typeof depth === 'undefined') { depth = 0; } - if (depth >= maxDepth && maxDepth > 0 && typeof obj === 'object') { - return isArray(obj) ? '[Array]' : '[Object]'; - } - - var indent = getIndent(opts, depth); - - if (typeof seen === 'undefined') { - seen = []; - } else if (indexOf(seen, obj) >= 0) { - return '[Circular]'; - } - - function inspect(value, from, noIndent) { - if (from) { - seen = $arrSlice.call(seen); - seen.push(from); - } - if (noIndent) { - var newOpts = { - depth: opts.depth - }; - if (has(opts, 'quoteStyle')) { - newOpts.quoteStyle = opts.quoteStyle; - } - return inspect_(value, newOpts, depth + 1, seen); - } - return inspect_(value, opts, depth + 1, seen); - } - - if (typeof obj === 'function' && !isRegExp(obj)) { // in older engines, regexes are callable - var name = nameOf(obj); - var keys = arrObjKeys(obj, inspect); - return '[Function' + (name ? ': ' + name : ' (anonymous)') + ']' + (keys.length > 0 ? ' { ' + $join.call(keys, ', ') + ' }' : ''); - } - if (isSymbol(obj)) { - var symString = hasShammedSymbols ? $replace.call(String(obj), /^(Symbol\(.*\))_[^)]*$/, '$1') : symToString.call(obj); - return typeof obj === 'object' && !hasShammedSymbols ? markBoxed(symString) : symString; - } - if (isElement(obj)) { - var s = '<' + $toLowerCase.call(String(obj.nodeName)); - var attrs = obj.attributes || []; - for (var i = 0; i < attrs.length; i++) { - s += ' ' + attrs[i].name + '=' + wrapQuotes(quote(attrs[i].value), 'double', opts); - } - s += '>'; - if (obj.childNodes && obj.childNodes.length) { s += '...'; } - s += ''; - return s; - } - if (isArray(obj)) { - if (obj.length === 0) { return '[]'; } - var xs = arrObjKeys(obj, inspect); - if (indent && !singleLineValues(xs)) { - return '[' + indentedJoin(xs, indent) + ']'; - } - return '[ ' + $join.call(xs, ', ') + ' ]'; - } - if (isError(obj)) { - var parts = arrObjKeys(obj, inspect); - if (!('cause' in Error.prototype) && 'cause' in obj && !isEnumerable.call(obj, 'cause')) { - return '{ [' + String(obj) + '] ' + $join.call($concat.call('[cause]: ' + inspect(obj.cause), parts), ', ') + ' }'; - } - if (parts.length === 0) { return '[' + String(obj) + ']'; } - return '{ [' + String(obj) + '] ' + $join.call(parts, ', ') + ' }'; - } - if (typeof obj === 'object' && customInspect) { - if (inspectSymbol && typeof obj[inspectSymbol] === 'function' && utilInspect) { - return utilInspect(obj, { depth: maxDepth - depth }); - } else if (customInspect !== 'symbol' && typeof obj.inspect === 'function') { - return obj.inspect(); - } - } - if (isMap(obj)) { - var mapParts = []; - mapForEach.call(obj, function (value, key) { - mapParts.push(inspect(key, obj, true) + ' => ' + inspect(value, obj)); - }); - return collectionOf('Map', mapSize.call(obj), mapParts, indent); - } - if (isSet(obj)) { - var setParts = []; - setForEach.call(obj, function (value) { - setParts.push(inspect(value, obj)); - }); - return collectionOf('Set', setSize.call(obj), setParts, indent); - } - if (isWeakMap(obj)) { - return weakCollectionOf('WeakMap'); - } - if (isWeakSet(obj)) { - return weakCollectionOf('WeakSet'); - } - if (isWeakRef(obj)) { - return weakCollectionOf('WeakRef'); - } - if (isNumber(obj)) { - return markBoxed(inspect(Number(obj))); - } - if (isBigInt(obj)) { - return markBoxed(inspect(bigIntValueOf.call(obj))); - } - if (isBoolean(obj)) { - return markBoxed(booleanValueOf.call(obj)); - } - if (isString(obj)) { - return markBoxed(inspect(String(obj))); - } - if (!isDate(obj) && !isRegExp(obj)) { - var ys = arrObjKeys(obj, inspect); - var isPlainObject = gPO ? gPO(obj) === Object.prototype : obj instanceof Object || obj.constructor === Object; - var protoTag = obj instanceof Object ? '' : 'null prototype'; - var stringTag = !isPlainObject && toStringTag && Object(obj) === obj && toStringTag in obj ? $slice.call(toStr(obj), 8, -1) : protoTag ? 'Object' : ''; - var constructorTag = isPlainObject || typeof obj.constructor !== 'function' ? '' : obj.constructor.name ? obj.constructor.name + ' ' : ''; - var tag = constructorTag + (stringTag || protoTag ? '[' + $join.call($concat.call([], stringTag || [], protoTag || []), ': ') + '] ' : ''); - if (ys.length === 0) { return tag + '{}'; } - if (indent) { - return tag + '{' + indentedJoin(ys, indent) + '}'; - } - return tag + '{ ' + $join.call(ys, ', ') + ' }'; - } - return String(obj); -}; - -function wrapQuotes(s, defaultStyle, opts) { - var quoteChar = (opts.quoteStyle || defaultStyle) === 'double' ? '"' : "'"; - return quoteChar + s + quoteChar; -} - -function quote(s) { - return $replace.call(String(s), /"/g, '"'); -} - -function isArray(obj) { return toStr(obj) === '[object Array]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); } -function isDate(obj) { return toStr(obj) === '[object Date]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); } -function isRegExp(obj) { return toStr(obj) === '[object RegExp]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); } -function isError(obj) { return toStr(obj) === '[object Error]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); } -function isString(obj) { return toStr(obj) === '[object String]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); } -function isNumber(obj) { return toStr(obj) === '[object Number]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); } -function isBoolean(obj) { return toStr(obj) === '[object Boolean]' && (!toStringTag || !(typeof obj === 'object' && toStringTag in obj)); } - -// Symbol and BigInt do have Symbol.toStringTag by spec, so that can't be used to eliminate false positives -function isSymbol(obj) { - if (hasShammedSymbols) { - return obj && typeof obj === 'object' && obj instanceof Symbol; - } - if (typeof obj === 'symbol') { - return true; - } - if (!obj || typeof obj !== 'object' || !symToString) { - return false; - } - try { - symToString.call(obj); - return true; - } catch (e) {} - return false; -} - -function isBigInt(obj) { - if (!obj || typeof obj !== 'object' || !bigIntValueOf) { - return false; - } - try { - bigIntValueOf.call(obj); - return true; - } catch (e) {} - return false; -} - -var hasOwn = Object.prototype.hasOwnProperty || function (key) { return key in this; }; -function has(obj, key) { - return hasOwn.call(obj, key); -} - -function toStr(obj) { - return objectToString.call(obj); -} - -function nameOf(f) { - if (f.name) { return f.name; } - var m = $match.call(functionToString.call(f), /^function\s*([\w$]+)/); - if (m) { return m[1]; } - return null; -} - -function indexOf(xs, x) { - if (xs.indexOf) { return xs.indexOf(x); } - for (var i = 0, l = xs.length; i < l; i++) { - if (xs[i] === x) { return i; } - } - return -1; -} - -function isMap(x) { - if (!mapSize || !x || typeof x !== 'object') { - return false; - } - try { - mapSize.call(x); - try { - setSize.call(x); - } catch (s) { - return true; - } - return x instanceof Map; // core-js workaround, pre-v2.5.0 - } catch (e) {} - return false; -} - -function isWeakMap(x) { - if (!weakMapHas || !x || typeof x !== 'object') { - return false; - } - try { - weakMapHas.call(x, weakMapHas); - try { - weakSetHas.call(x, weakSetHas); - } catch (s) { - return true; - } - return x instanceof WeakMap; // core-js workaround, pre-v2.5.0 - } catch (e) {} - return false; -} - -function isWeakRef(x) { - if (!weakRefDeref || !x || typeof x !== 'object') { - return false; - } - try { - weakRefDeref.call(x); - return true; - } catch (e) {} - return false; -} - -function isSet(x) { - if (!setSize || !x || typeof x !== 'object') { - return false; - } - try { - setSize.call(x); - try { - mapSize.call(x); - } catch (m) { - return true; - } - return x instanceof Set; // core-js workaround, pre-v2.5.0 - } catch (e) {} - return false; -} - -function isWeakSet(x) { - if (!weakSetHas || !x || typeof x !== 'object') { - return false; - } - try { - weakSetHas.call(x, weakSetHas); - try { - weakMapHas.call(x, weakMapHas); - } catch (s) { - return true; - } - return x instanceof WeakSet; // core-js workaround, pre-v2.5.0 - } catch (e) {} - return false; -} - -function isElement(x) { - if (!x || typeof x !== 'object') { return false; } - if (typeof HTMLElement !== 'undefined' && x instanceof HTMLElement) { - return true; - } - return typeof x.nodeName === 'string' && typeof x.getAttribute === 'function'; -} - -function inspectString(str, opts) { - if (str.length > opts.maxStringLength) { - var remaining = str.length - opts.maxStringLength; - var trailer = '... ' + remaining + ' more character' + (remaining > 1 ? 's' : ''); - return inspectString($slice.call(str, 0, opts.maxStringLength), opts) + trailer; - } - // eslint-disable-next-line no-control-regex - var s = $replace.call($replace.call(str, /(['\\])/g, '\\$1'), /[\x00-\x1f]/g, lowbyte); - return wrapQuotes(s, 'single', opts); -} - -function lowbyte(c) { - var n = c.charCodeAt(0); - var x = { - 8: 'b', - 9: 't', - 10: 'n', - 12: 'f', - 13: 'r' - }[n]; - if (x) { return '\\' + x; } - return '\\x' + (n < 0x10 ? '0' : '') + $toUpperCase.call(n.toString(16)); -} - -function markBoxed(str) { - return 'Object(' + str + ')'; -} - -function weakCollectionOf(type) { - return type + ' { ? }'; -} - -function collectionOf(type, size, entries, indent) { - var joinedEntries = indent ? indentedJoin(entries, indent) : $join.call(entries, ', '); - return type + ' (' + size + ') {' + joinedEntries + '}'; -} - -function singleLineValues(xs) { - for (var i = 0; i < xs.length; i++) { - if (indexOf(xs[i], '\n') >= 0) { - return false; - } - } - return true; -} - -function getIndent(opts, depth) { - var baseIndent; - if (opts.indent === '\t') { - baseIndent = '\t'; - } else if (typeof opts.indent === 'number' && opts.indent > 0) { - baseIndent = $join.call(Array(opts.indent + 1), ' '); - } else { - return null; - } - return { - base: baseIndent, - prev: $join.call(Array(depth + 1), baseIndent) - }; -} - -function indentedJoin(xs, indent) { - if (xs.length === 0) { return ''; } - var lineJoiner = '\n' + indent.prev + indent.base; - return lineJoiner + $join.call(xs, ',' + lineJoiner) + '\n' + indent.prev; -} - -function arrObjKeys(obj, inspect) { - var isArr = isArray(obj); - var xs = []; - if (isArr) { - xs.length = obj.length; - for (var i = 0; i < obj.length; i++) { - xs[i] = has(obj, i) ? inspect(obj[i], obj) : ''; - } - } - var syms = typeof gOPS === 'function' ? gOPS(obj) : []; - var symMap; - if (hasShammedSymbols) { - symMap = {}; - for (var k = 0; k < syms.length; k++) { - symMap['$' + syms[k]] = syms[k]; - } - } - - for (var key in obj) { // eslint-disable-line no-restricted-syntax - if (!has(obj, key)) { continue; } // eslint-disable-line no-restricted-syntax, no-continue - if (isArr && String(Number(key)) === key && key < obj.length) { continue; } // eslint-disable-line no-restricted-syntax, no-continue - if (hasShammedSymbols && symMap['$' + key] instanceof Symbol) { - // this is to prevent shammed Symbols, which are stored as strings, from being included in the string key section - continue; // eslint-disable-line no-restricted-syntax, no-continue - } else if ($test.call(/[^\w$]/, key)) { - xs.push(inspect(key, obj) + ': ' + inspect(obj[key], obj)); - } else { - xs.push(key + ': ' + inspect(obj[key], obj)); - } - } - if (typeof gOPS === 'function') { - for (var j = 0; j < syms.length; j++) { - if (isEnumerable.call(obj, syms[j])) { - xs.push('[' + inspect(syms[j]) + ']: ' + inspect(obj[syms[j]], obj)); - } - } - } - return xs; -} - -},{"./util.inspect":6}],16:[function(require,module,exports){ -'use strict'; - -var GetIntrinsic = require('get-intrinsic'); -var callBound = require('call-bind/callBound'); -var inspect = require('object-inspect'); - -var $TypeError = GetIntrinsic('%TypeError%'); -var $WeakMap = GetIntrinsic('%WeakMap%', true); -var $Map = GetIntrinsic('%Map%', true); - -var $weakMapGet = callBound('WeakMap.prototype.get', true); -var $weakMapSet = callBound('WeakMap.prototype.set', true); -var $weakMapHas = callBound('WeakMap.prototype.has', true); -var $mapGet = callBound('Map.prototype.get', true); -var $mapSet = callBound('Map.prototype.set', true); -var $mapHas = callBound('Map.prototype.has', true); - -/* - * This function traverses the list returning the node corresponding to the - * given key. - * - * That node is also moved to the head of the list, so that if it's accessed - * again we don't need to traverse the whole list. By doing so, all the recently - * used nodes can be accessed relatively quickly. - */ -var listGetNode = function (list, key) { // eslint-disable-line consistent-return - for (var prev = list, curr; (curr = prev.next) !== null; prev = curr) { - if (curr.key === key) { - prev.next = curr.next; - curr.next = list.next; - list.next = curr; // eslint-disable-line no-param-reassign - return curr; - } - } -}; - -var listGet = function (objects, key) { - var node = listGetNode(objects, key); - return node && node.value; -}; -var listSet = function (objects, key, value) { - var node = listGetNode(objects, key); - if (node) { - node.value = value; - } else { - // Prepend the new node to the beginning of the list - objects.next = { // eslint-disable-line no-param-reassign - key: key, - next: objects.next, - value: value - }; - } -}; -var listHas = function (objects, key) { - return !!listGetNode(objects, key); -}; - -module.exports = function getSideChannel() { - var $wm; - var $m; - var $o; - var channel = { - assert: function (key) { - if (!channel.has(key)) { - throw new $TypeError('Side channel does not contain ' + inspect(key)); - } - }, - get: function (key) { // eslint-disable-line consistent-return - if ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) { - if ($wm) { - return $weakMapGet($wm, key); - } - } else if ($Map) { - if ($m) { - return $mapGet($m, key); - } - } else { - if ($o) { // eslint-disable-line no-lonely-if - return listGet($o, key); - } - } - }, - has: function (key) { - if ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) { - if ($wm) { - return $weakMapHas($wm, key); - } - } else if ($Map) { - if ($m) { - return $mapHas($m, key); - } - } else { - if ($o) { // eslint-disable-line no-lonely-if - return listHas($o, key); - } - } - return false; - }, - set: function (key, value) { - if ($WeakMap && key && (typeof key === 'object' || typeof key === 'function')) { - if (!$wm) { - $wm = new $WeakMap(); - } - $weakMapSet($wm, key, value); - } else if ($Map) { - if (!$m) { - $m = new $Map(); - } - $mapSet($m, key, value); - } else { - if (!$o) { - /* - * Initialize the linked list as an empty node, so that we don't have - * to special-case handling of the first node: we can always refer to - * it as (previous node).next, instead of something like (list).head - */ - $o = { key: {}, next: null }; - } - listSet($o, key, value); - } - } - }; - return channel; -}; - -},{"call-bind/callBound":7,"get-intrinsic":11,"object-inspect":15}]},{},[2])(2) -}); diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/qs/LICENSE.md thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/qs/LICENSE.md --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/qs/LICENSE.md 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/qs/LICENSE.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors) -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/README.md thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/README.md --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/README.md 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/README.md 2023-04-11 06:11:52.000000000 +0000 @@ -1,19 +1,19 @@ This directory contains the Matrix Client-Server SDK for Javascript available -at https://github.com/matrix-org/matrix-js-sdk/. Current version is v20.0.0. +at https://github.com/matrix-org/matrix-js-sdk/. Current version is v24.0.0. The following npm dependencies are included: -* @matrix-org/olm: https://gitlab.matrix.org/matrix-org/olm/-/packages/10 v3.2.12 +* @matrix-org/olm: https://gitlab.matrix.org/matrix-org/olm/-/packages?type=npm v3.2.14 * another-json: https://www.npmjs.com/package/another-json/ v0.2.0 * base-x: https://www.npmjs.com/package/base-x v4.0.0 -* browser-request: https://www.npmjs.com/package/browser-request v0.3.3 * bs58: https://www.npmjs.com/package/bs58 v5.0.0 -* content-type: https://www.npmjs.com/package/content-type v1.0.4 +* content-type: https://www.npmjs.com/package/content-type v1.0.5 * events: https://www.npmjs.com/package/events v3.3.0 -* matrix-events-sdk: https://www.npmjs.com/package/matrix-events-sdk v0.0.1-beta.7 +* matrix-events-sdk: https://www.npmjs.com/package/matrix-events-sdk v0.0.1 +* matrix-widget-api: https://www.npmjs.com/package/matrix-widget-api v1.1.1 * p-retry: https://www.npmjs.com/package/p-retry v4.6.2 -* qs: https://www.npmjs.com/package/qs v6.11.0 * retry: https://www.npmjs.com/package/retry v0.13.1 +* sdp-transform: https://www.npmjs.com/package/sdp-transform v2.14.1 * unhomoglyph: https://www.npmjs.com/package/unhomoglyph v1.0.6 The following npm dependencies are shimmed: @@ -21,7 +21,7 @@ * loglevel: The chat framework's logging methods are used internally. * safe-buffer: A buffer shim, initially modeled after the safe-buffer NPM package, now used to provide a Buffer object to the crypto stack. -* url: The global URL object is used directly. +* uuid: Only the v4 is provided via cryto.randomUUID(). There is not any automated way to update the libraries. @@ -29,31 +29,32 @@ using yarn to obtain the dependencies), and then compiling the SDK using Babel. To make the whole thing work, some file paths and global variables are defined -in chat/protocols/matrix/matrix-sdk.jsm. +in `chat/protocols/matrix/matrix-sdk.sys.mjs`. ## Updating matrix-js-sdk 1. Download the matrix-js-sdk repository from https://github.com/matrix-org/matrix-js-sdk/. 2. Modify `.babelrc` (see below). -3. Run yarn install -4. Run Babel in the matrix-js-sdk checkout: +3. (If this is an old checkout, remove any previous artifacts. Run `rm -r lib; rm -r node_modules`.) +4. Run `yarn install`. +5. Run Babel in the matrix-js-sdk checkout: `./node_modules/.bin/babel --source-maps false -d lib --extensions ".ts,.js" src` (at time of writing identical to `yarn build:compile`) -5. The following commands assume you're in mozilla-central/comm and that the +6. The following commands assume you're in mozilla-central/comm and that the matrix-js-sdk is checked out next to mozilla-central. -6. Remove the old SDK files `hg rm chat/protocols/matrix/lib/matrix-sdk` -7. Undo the removal of the license: `hg revert chat/protocols/matrix/lib/matrix-sdk/LICENSE` -8. Copy the Babel-ified JavaScript files from the matrix-js-sdk to vendored +7. Remove the old SDK files `hg rm chat/protocols/matrix/lib/matrix-sdk` +9. Undo the removal of the license: `hg revert chat/protocols/matrix/lib/matrix-sdk/LICENSE` +0. Copy the Babel-ified JavaScript files from the matrix-js-sdk to vendored location: `cp -r ../../matrix-js-sdk/lib/* chat/protocols/matrix/lib/matrix-sdk` -9. Add the files back to Mercurial: `hg add chat/protocols/matrix/lib/matrix-sdk` -10. Modify `moz.build` to add/remove/rename modified files. Note that some - modules that have no actual contents (just an empty export) are not - currently included. -11. Modify `matrix-sdk.jsm` to add/remove/rename modified files. +10. Add the files back to Mercurial: `hg add chat/protocols/matrix/lib/matrix-sdk` +11. Modify `chat/protocols/matrix/lib/moz.build` to add/remove/rename modified + files. Note that some modules that have no actual contents (just an empty + export) are not currently included. +12. Modify `matrix-sdk.sys.mjs` to add/remove/rename any changed modules. ### Custom `.babelrc` -By default the matrix-js-sdk targets a version of ECMAScript that is far below +By default, the matrix-js-sdk targets a version of ECMAScript that is far below what Gecko supports, this causes lots of additional processing to occur (e.g. converting async functions, etc.) To disable this, a custom `.babelrc` file is used: @@ -96,10 +97,12 @@ named for the package or named index.js. This should get copied to the proper sub-directory. -### Updating browser-request - -Follow the directions for updating single file dependencies, then modify the -index.js file so that the `is_crossDomain` always returns `false`. +``` +cp ../../matrix-js-sdk/node_modules/another-json/another-json.js chat/protocols/matrix/lib/another-json +cp ../../matrix-js-sdk/node_modules/base-x/src/index.js chat/protocols/matrix/lib/base-x +cp ../../matrix-js-sdk/node_modules/bs58/index.js chat/protocols/matrix/lib/bs58 +cp ../../matrix-js-sdk/node_modules/content-type/index.js chat/protocols/matrix/lib/content-type +``` ### Updating events @@ -112,19 +115,32 @@ We only want the JS modules. So we want all the js files in `lib/**/*.js` from the package. -### Updating qs +### Updating matrix-widget-api + +The matrix-widget-api includes raw JS modules and Typescript definition files. +We only want the JS modules. So we want all the js files in `lib/**/*.js` +from the package. + +### Updating sdp-transform + +The sdp-transform package includes raw JS modules, so we want all the js files +under `lib/*.js`. -The qs package comes with two valid entry points, `dist/qs.js` and -`lib/index.js`. The `dist` one is already prepared for use in browsers -but still supports being loaded as commonJS module and it is only a single -file, so we prefer that one. +``` +cp ../../matrix-js-sdk/node_modules/sdp-transform/lib/*.js chat/protocols/matrix/lib/sdp-transform +``` ### Updating unhomoglyph -This is simlar to the single file dependencies, but also has a JSON data file. +This is similar to the single file dependencies, but also has a JSON data file. Both of these files should be copied to the unhomoglyph directory. -### Updating loglevel, safe-buffer, url +``` +cp ../../matrix-js-sdk/node_modules/unhomoglyph/index.js chat/protocols/matrix/lib/unhomoglyph +cp ../../matrix-js-sdk/node_modules/unhomoglyph/data.json chat/protocols/matrix/lib/unhomoglyph +``` + +### Updating loglevel, safe-buffer, uuid These packages have an alternate implementation in the `../shims` directory and thus are not included here. @@ -132,7 +148,8 @@ ### Updating olm The package is published on the Matrix gitlab. To update the library, download -the latest `.tgz` bundle and replace the files in the `@matrix-org/olm` folder. +the latest `.tgz` bundle and replace the `olm.js` and `olm.wasm` files in the +`@matrix-org/olm` folder. ### Updating p-retry @@ -140,3 +157,9 @@ the `retry` package, which consists of three files, and `index.js` and two modules in the `lib` folder. All four files should be mirrored over into this folder into a `p-retry` and `retry` folder respectively. + +``` +cp ../../matrix-js-sdk/node_modules/p-retry/index.js chat/protocols/matrix/lib/p-retry/ +cp ../../matrix-js-sdk/node_modules/retry/index.js chat/protocols/matrix/lib/retry +cp ../../matrix-js-sdk/node_modules/retry/lib/*.js chat/protocols/matrix/lib/retry/lib +``` diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/grammar.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/grammar.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/grammar.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/grammar.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,494 @@ +var grammar = module.exports = { + v: [{ + name: 'version', + reg: /^(\d*)$/ + }], + o: [{ + // o=- 20518 0 IN IP4 203.0.113.1 + // NB: sessionId will be a String in most cases because it is huge + name: 'origin', + reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/, + names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'], + format: '%s %s %d %s IP%d %s' + }], + // default parsing of these only (though some of these feel outdated) + s: [{ name: 'name' }], + i: [{ name: 'description' }], + u: [{ name: 'uri' }], + e: [{ name: 'email' }], + p: [{ name: 'phone' }], + z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly... + r: [{ name: 'repeats' }], // TODO: this one can also be parsed properly + // k: [{}], // outdated thing ignored + t: [{ + // t=0 0 + name: 'timing', + reg: /^(\d*) (\d*)/, + names: ['start', 'stop'], + format: '%d %d' + }], + c: [{ + // c=IN IP4 10.47.197.26 + name: 'connection', + reg: /^IN IP(\d) (\S*)/, + names: ['version', 'ip'], + format: 'IN IP%d %s' + }], + b: [{ + // b=AS:4000 + push: 'bandwidth', + reg: /^(TIAS|AS|CT|RR|RS):(\d*)/, + names: ['type', 'limit'], + format: '%s:%s' + }], + m: [{ + // m=video 51744 RTP/AVP 126 97 98 34 31 + // NB: special - pushes to session + // TODO: rtp/fmtp should be filtered by the payloads found here? + reg: /^(\w*) (\d*) ([\w/]*)(?: (.*))?/, + names: ['type', 'port', 'protocol', 'payloads'], + format: '%s %d %s %s' + }], + a: [ + { + // a=rtpmap:110 opus/48000/2 + push: 'rtp', + reg: /^rtpmap:(\d*) ([\w\-.]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/, + names: ['payload', 'codec', 'rate', 'encoding'], + format: function (o) { + return (o.encoding) + ? 'rtpmap:%d %s/%s/%s' + : o.rate + ? 'rtpmap:%d %s/%s' + : 'rtpmap:%d %s'; + } + }, + { + // a=fmtp:108 profile-level-id=24;object=23;bitrate=64000 + // a=fmtp:111 minptime=10; useinbandfec=1 + push: 'fmtp', + reg: /^fmtp:(\d*) ([\S| ]*)/, + names: ['payload', 'config'], + format: 'fmtp:%d %s' + }, + { + // a=control:streamid=0 + name: 'control', + reg: /^control:(.*)/, + format: 'control:%s' + }, + { + // a=rtcp:65179 IN IP4 193.84.77.194 + name: 'rtcp', + reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/, + names: ['port', 'netType', 'ipVer', 'address'], + format: function (o) { + return (o.address != null) + ? 'rtcp:%d %s IP%d %s' + : 'rtcp:%d'; + } + }, + { + // a=rtcp-fb:98 trr-int 100 + push: 'rtcpFbTrrInt', + reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/, + names: ['payload', 'value'], + format: 'rtcp-fb:%s trr-int %d' + }, + { + // a=rtcp-fb:98 nack rpsi + push: 'rtcpFb', + reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/, + names: ['payload', 'type', 'subtype'], + format: function (o) { + return (o.subtype != null) + ? 'rtcp-fb:%s %s %s' + : 'rtcp-fb:%s %s'; + } + }, + { + // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset + // a=extmap:1/recvonly URI-gps-string + // a=extmap:3 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:smpte-tc 25@600/24 + push: 'ext', + reg: /^extmap:(\d+)(?:\/(\w+))?(?: (urn:ietf:params:rtp-hdrext:encrypt))? (\S*)(?: (\S*))?/, + names: ['value', 'direction', 'encrypt-uri', 'uri', 'config'], + format: function (o) { + return ( + 'extmap:%d' + + (o.direction ? '/%s' : '%v') + + (o['encrypt-uri'] ? ' %s' : '%v') + + ' %s' + + (o.config ? ' %s' : '') + ); + } + }, + { + // a=extmap-allow-mixed + name: 'extmapAllowMixed', + reg: /^(extmap-allow-mixed)/ + }, + { + // a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 + push: 'crypto', + reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/, + names: ['id', 'suite', 'config', 'sessionConfig'], + format: function (o) { + return (o.sessionConfig != null) + ? 'crypto:%d %s %s %s' + : 'crypto:%d %s %s'; + } + }, + { + // a=setup:actpass + name: 'setup', + reg: /^setup:(\w*)/, + format: 'setup:%s' + }, + { + // a=connection:new + name: 'connectionType', + reg: /^connection:(new|existing)/, + format: 'connection:%s' + }, + { + // a=mid:1 + name: 'mid', + reg: /^mid:([^\s]*)/, + format: 'mid:%s' + }, + { + // a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a + name: 'msid', + reg: /^msid:(.*)/, + format: 'msid:%s' + }, + { + // a=ptime:20 + name: 'ptime', + reg: /^ptime:(\d*(?:\.\d*)*)/, + format: 'ptime:%d' + }, + { + // a=maxptime:60 + name: 'maxptime', + reg: /^maxptime:(\d*(?:\.\d*)*)/, + format: 'maxptime:%d' + }, + { + // a=sendrecv + name: 'direction', + reg: /^(sendrecv|recvonly|sendonly|inactive)/ + }, + { + // a=ice-lite + name: 'icelite', + reg: /^(ice-lite)/ + }, + { + // a=ice-ufrag:F7gI + name: 'iceUfrag', + reg: /^ice-ufrag:(\S*)/, + format: 'ice-ufrag:%s' + }, + { + // a=ice-pwd:x9cml/YzichV2+XlhiMu8g + name: 'icePwd', + reg: /^ice-pwd:(\S*)/, + format: 'ice-pwd:%s' + }, + { + // a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33 + name: 'fingerprint', + reg: /^fingerprint:(\S*) (\S*)/, + names: ['type', 'hash'], + format: 'fingerprint:%s %s' + }, + { + // a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host + // a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 network-id 3 network-cost 10 + // a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 network-id 3 network-cost 10 + // a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 network-id 3 network-cost 10 + // a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 network-id 3 network-cost 10 + push:'candidates', + reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?(?: network-id (\d*))?(?: network-cost (\d*))?/, + names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation', 'network-id', 'network-cost'], + format: function (o) { + var str = 'candidate:%s %d %s %d %s %d typ %s'; + + str += (o.raddr != null) ? ' raddr %s rport %d' : '%v%v'; + + // NB: candidate has three optional chunks, so %void middles one if it's missing + str += (o.tcptype != null) ? ' tcptype %s' : '%v'; + + if (o.generation != null) { + str += ' generation %d'; + } + + str += (o['network-id'] != null) ? ' network-id %d' : '%v'; + str += (o['network-cost'] != null) ? ' network-cost %d' : '%v'; + return str; + } + }, + { + // a=end-of-candidates (keep after the candidates line for readability) + name: 'endOfCandidates', + reg: /^(end-of-candidates)/ + }, + { + // a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ... + name: 'remoteCandidates', + reg: /^remote-candidates:(.*)/, + format: 'remote-candidates:%s' + }, + { + // a=ice-options:google-ice + name: 'iceOptions', + reg: /^ice-options:(\S*)/, + format: 'ice-options:%s' + }, + { + // a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 + push: 'ssrcs', + reg: /^ssrc:(\d*) ([\w_-]*)(?::(.*))?/, + names: ['id', 'attribute', 'value'], + format: function (o) { + var str = 'ssrc:%d'; + if (o.attribute != null) { + str += ' %s'; + if (o.value != null) { + str += ':%s'; + } + } + return str; + } + }, + { + // a=ssrc-group:FEC 1 2 + // a=ssrc-group:FEC-FR 3004364195 1080772241 + push: 'ssrcGroups', + // token-char = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7E + reg: /^ssrc-group:([\x21\x23\x24\x25\x26\x27\x2A\x2B\x2D\x2E\w]*) (.*)/, + names: ['semantics', 'ssrcs'], + format: 'ssrc-group:%s %s' + }, + { + // a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV + name: 'msidSemantic', + reg: /^msid-semantic:\s?(\w*) (\S*)/, + names: ['semantic', 'token'], + format: 'msid-semantic: %s %s' // space after ':' is not accidental + }, + { + // a=group:BUNDLE audio video + push: 'groups', + reg: /^group:(\w*) (.*)/, + names: ['type', 'mids'], + format: 'group:%s %s' + }, + { + // a=rtcp-mux + name: 'rtcpMux', + reg: /^(rtcp-mux)/ + }, + { + // a=rtcp-rsize + name: 'rtcpRsize', + reg: /^(rtcp-rsize)/ + }, + { + // a=sctpmap:5000 webrtc-datachannel 1024 + name: 'sctpmap', + reg: /^sctpmap:([\w_/]*) (\S*)(?: (\S*))?/, + names: ['sctpmapNumber', 'app', 'maxMessageSize'], + format: function (o) { + return (o.maxMessageSize != null) + ? 'sctpmap:%s %s %s' + : 'sctpmap:%s %s'; + } + }, + { + // a=x-google-flag:conference + name: 'xGoogleFlag', + reg: /^x-google-flag:([^\s]*)/, + format: 'x-google-flag:%s' + }, + { + // a=rid:1 send max-width=1280;max-height=720;max-fps=30;depend=0 + push: 'rids', + reg: /^rid:([\d\w]+) (\w+)(?: ([\S| ]*))?/, + names: ['id', 'direction', 'params'], + format: function (o) { + return (o.params) ? 'rid:%s %s %s' : 'rid:%s %s'; + } + }, + { + // a=imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250] + // a=imageattr:* send [x=800,y=640] recv * + // a=imageattr:100 recv [x=320,y=240] + push: 'imageattrs', + reg: new RegExp( + // a=imageattr:97 + '^imageattr:(\\d+|\\*)' + + // send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] + '[\\s\\t]+(send|recv)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*)' + + // recv [x=330,y=250] + '(?:[\\s\\t]+(recv|send)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*))?' + ), + names: ['pt', 'dir1', 'attrs1', 'dir2', 'attrs2'], + format: function (o) { + return 'imageattr:%s %s %s' + (o.dir2 ? ' %s %s' : ''); + } + }, + { + // a=simulcast:send 1,2,3;~4,~5 recv 6;~7,~8 + // a=simulcast:recv 1;4,5 send 6;7 + name: 'simulcast', + reg: new RegExp( + // a=simulcast: + '^simulcast:' + + // send 1,2,3;~4,~5 + '(send|recv) ([a-zA-Z0-9\\-_~;,]+)' + + // space + recv 6;~7,~8 + '(?:\\s?(send|recv) ([a-zA-Z0-9\\-_~;,]+))?' + + // end + '$' + ), + names: ['dir1', 'list1', 'dir2', 'list2'], + format: function (o) { + return 'simulcast:%s %s' + (o.dir2 ? ' %s %s' : ''); + } + }, + { + // old simulcast draft 03 (implemented by Firefox) + // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-03 + // a=simulcast: recv pt=97;98 send pt=97 + // a=simulcast: send rid=5;6;7 paused=6,7 + name: 'simulcast_03', + reg: /^simulcast:[\s\t]+([\S+\s\t]+)$/, + names: ['value'], + format: 'simulcast: %s' + }, + { + // a=framerate:25 + // a=framerate:29.97 + name: 'framerate', + reg: /^framerate:(\d+(?:$|\.\d+))/, + format: 'framerate:%s' + }, + { + // RFC4570 + // a=source-filter: incl IN IP4 239.5.2.31 10.1.15.5 + name: 'sourceFilter', + reg: /^source-filter: *(excl|incl) (\S*) (IP4|IP6|\*) (\S*) (.*)/, + names: ['filterMode', 'netType', 'addressTypes', 'destAddress', 'srcList'], + format: 'source-filter: %s %s %s %s %s' + }, + { + // a=bundle-only + name: 'bundleOnly', + reg: /^(bundle-only)/ + }, + { + // a=label:1 + name: 'label', + reg: /^label:(.+)/, + format: 'label:%s' + }, + { + // RFC version 26 for SCTP over DTLS + // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-5 + name: 'sctpPort', + reg: /^sctp-port:(\d+)$/, + format: 'sctp-port:%s' + }, + { + // RFC version 26 for SCTP over DTLS + // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-6 + name: 'maxMessageSize', + reg: /^max-message-size:(\d+)$/, + format: 'max-message-size:%s' + }, + { + // RFC7273 + // a=ts-refclk:ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:37 + push:'tsRefClocks', + reg: /^ts-refclk:([^\s=]*)(?:=(\S*))?/, + names: ['clksrc', 'clksrcExt'], + format: function (o) { + return 'ts-refclk:%s' + (o.clksrcExt != null ? '=%s' : ''); + } + }, + { + // RFC7273 + // a=mediaclk:direct=963214424 + name:'mediaClk', + reg: /^mediaclk:(?:id=(\S*))? *([^\s=]*)(?:=(\S*))?(?: *rate=(\d+)\/(\d+))?/, + names: ['id', 'mediaClockName', 'mediaClockValue', 'rateNumerator', 'rateDenominator'], + format: function (o) { + var str = 'mediaclk:'; + str += (o.id != null ? 'id=%s %s' : '%v%s'); + str += (o.mediaClockValue != null ? '=%s' : ''); + str += (o.rateNumerator != null ? ' rate=%s' : ''); + str += (o.rateDenominator != null ? '/%s' : ''); + return str; + } + }, + { + // a=keywds:keywords + name: 'keywords', + reg: /^keywds:(.+)$/, + format: 'keywds:%s' + }, + { + // a=content:main + name: 'content', + reg: /^content:(.+)/, + format: 'content:%s' + }, + // BFCP https://tools.ietf.org/html/rfc4583 + { + // a=floorctrl:c-s + name: 'bfcpFloorCtrl', + reg: /^floorctrl:(c-only|s-only|c-s)/, + format: 'floorctrl:%s' + }, + { + // a=confid:1 + name: 'bfcpConfId', + reg: /^confid:(\d+)/, + format: 'confid:%s' + }, + { + // a=userid:1 + name: 'bfcpUserId', + reg: /^userid:(\d+)/, + format: 'userid:%s' + }, + { + // a=floorid:1 + name: 'bfcpFloorId', + reg: /^floorid:(.+) (?:m-stream|mstrm):(.+)/, + names: ['id', 'mStream'], + format: 'floorid:%s mstrm:%s' + }, + { + // any a= that we don't understand is kept verbatim on media.invalid + push: 'invalid', + names: ['value'] + } + ] +}; + +// set sensible defaults to avoid polluting the grammar with boring details +Object.keys(grammar).forEach(function (key) { + var objs = grammar[key]; + objs.forEach(function (obj) { + if (!obj.reg) { + obj.reg = /(.*)/; + } + if (!obj.format) { + obj.format = '%s'; + } + }); +}); diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/index.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/index.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/index.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/index.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,11 @@ +var parser = require('./parser'); +var writer = require('./writer'); + +exports.write = writer; +exports.parse = parser.parse; +exports.parseParams = parser.parseParams; +exports.parseFmtpConfig = parser.parseFmtpConfig; // Alias of parseParams(). +exports.parsePayloads = parser.parsePayloads; +exports.parseRemoteCandidates = parser.parseRemoteCandidates; +exports.parseImageAttributes = parser.parseImageAttributes; +exports.parseSimulcastStreamList = parser.parseSimulcastStreamList; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/LICENSE thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/LICENSE --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/LICENSE 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2013 Eirik Albrigtsen + +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 -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/parser.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/parser.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/parser.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/parser.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,124 @@ +var toIntIfInt = function (v) { + return String(Number(v)) === v ? Number(v) : v; +}; + +var attachProperties = function (match, location, names, rawName) { + if (rawName && !names) { + location[rawName] = toIntIfInt(match[1]); + } + else { + for (var i = 0; i < names.length; i += 1) { + if (match[i+1] != null) { + location[names[i]] = toIntIfInt(match[i+1]); + } + } + } +}; + +var parseReg = function (obj, location, content) { + var needsBlank = obj.name && obj.names; + if (obj.push && !location[obj.push]) { + location[obj.push] = []; + } + else if (needsBlank && !location[obj.name]) { + location[obj.name] = {}; + } + var keyLocation = obj.push ? + {} : // blank object that will be pushed + needsBlank ? location[obj.name] : location; // otherwise, named location or root + + attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name); + + if (obj.push) { + location[obj.push].push(keyLocation); + } +}; + +var grammar = require('./grammar'); +var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/); + +exports.parse = function (sdp) { + var session = {} + , media = [] + , location = session; // points at where properties go under (one of the above) + + // parse lines we understand + sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) { + var type = l[0]; + var content = l.slice(2); + if (type === 'm') { + media.push({rtp: [], fmtp: []}); + location = media[media.length-1]; // point at latest media line + } + + for (var j = 0; j < (grammar[type] || []).length; j += 1) { + var obj = grammar[type][j]; + if (obj.reg.test(content)) { + return parseReg(obj, location, content); + } + } + }); + + session.media = media; // link it up + return session; +}; + +var paramReducer = function (acc, expr) { + var s = expr.split(/=(.+)/, 2); + if (s.length === 2) { + acc[s[0]] = toIntIfInt(s[1]); + } else if (s.length === 1 && expr.length > 1) { + acc[s[0]] = undefined; + } + return acc; +}; + +exports.parseParams = function (str) { + return str.split(/;\s?/).reduce(paramReducer, {}); +}; + +// For backward compatibility - alias will be removed in 3.0.0 +exports.parseFmtpConfig = exports.parseParams; + +exports.parsePayloads = function (str) { + return str.toString().split(' ').map(Number); +}; + +exports.parseRemoteCandidates = function (str) { + var candidates = []; + var parts = str.split(' ').map(toIntIfInt); + for (var i = 0; i < parts.length; i += 3) { + candidates.push({ + component: parts[i], + ip: parts[i + 1], + port: parts[i + 2] + }); + } + return candidates; +}; + +exports.parseImageAttributes = function (str) { + return str.split(' ').map(function (item) { + return item.substring(1, item.length-1).split(',').reduce(paramReducer, {}); + }); +}; + +exports.parseSimulcastStreamList = function (str) { + return str.split(';').map(function (stream) { + return stream.split(',').map(function (format) { + var scid, paused = false; + + if (format[0] !== '~') { + scid = toIntIfInt(format); + } else { + scid = toIntIfInt(format.substring(1, format.length)); + paused = true; + } + + return { + scid: scid, + paused: paused + }; + }); + }); +}; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/writer.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/writer.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/lib/sdp-transform/writer.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/lib/sdp-transform/writer.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,114 @@ +var grammar = require('./grammar'); + +// customized util.format - discards excess arguments and can void middle ones +var formatRegExp = /%[sdv%]/g; +var format = function (formatStr) { + var i = 1; + var args = arguments; + var len = args.length; + return formatStr.replace(formatRegExp, function (x) { + if (i >= len) { + return x; // missing argument + } + var arg = args[i]; + i += 1; + switch (x) { + case '%%': + return '%'; + case '%s': + return String(arg); + case '%d': + return Number(arg); + case '%v': + return ''; + } + }); + // NB: we discard excess arguments - they are typically undefined from makeLine +}; + +var makeLine = function (type, obj, location) { + var str = obj.format instanceof Function ? + (obj.format(obj.push ? location : location[obj.name])) : + obj.format; + + var args = [type + '=' + str]; + if (obj.names) { + for (var i = 0; i < obj.names.length; i += 1) { + var n = obj.names[i]; + if (obj.name) { + args.push(location[obj.name][n]); + } + else { // for mLine and push attributes + args.push(location[obj.names[i]]); + } + } + } + else { + args.push(location[obj.name]); + } + return format.apply(null, args); +}; + +// RFC specified order +// TODO: extend this with all the rest +var defaultOuterOrder = [ + 'v', 'o', 's', 'i', + 'u', 'e', 'p', 'c', + 'b', 't', 'r', 'z', 'a' +]; +var defaultInnerOrder = ['i', 'c', 'b', 'a']; + + +module.exports = function (session, opts) { + opts = opts || {}; + // ensure certain properties exist + if (session.version == null) { + session.version = 0; // 'v=0' must be there (only defined version atm) + } + if (session.name == null) { + session.name = ' '; // 's= ' must be there if no meaningful name set + } + session.media.forEach(function (mLine) { + if (mLine.payloads == null) { + mLine.payloads = ''; + } + }); + + var outerOrder = opts.outerOrder || defaultOuterOrder; + var innerOrder = opts.innerOrder || defaultInnerOrder; + var sdp = []; + + // loop through outerOrder for matching properties on session + outerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in session && session[obj.name] != null) { + sdp.push(makeLine(type, obj, session)); + } + else if (obj.push in session && session[obj.push] != null) { + session[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + + // then for each media line, follow the innerOrder + session.media.forEach(function (mLine) { + sdp.push(makeLine('m', grammar.m[0], mLine)); + + innerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in mLine && mLine[obj.name] != null) { + sdp.push(makeLine(type, obj, mLine)); + } + else if (obj.push in mLine && mLine[obj.push] != null) { + mLine[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + }); + + return sdp.join('\r\n') + '\r\n'; +}; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/matrixMessageContent.jsm thunderbird-102.10.0+build2/comm/chat/protocols/matrix/matrixMessageContent.jsm --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/matrixMessageContent.jsm 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/matrixMessageContent.jsm 2023-04-11 06:11:52.000000000 +0000 @@ -7,9 +7,7 @@ var { XPCOMUtils, l10nHelper } = ChromeUtils.import( "resource:///modules/imXPCOMUtils.jsm" ); -const { MatrixSDK, getHttpUriForMxc } = ChromeUtils.import( - "resource:///modules/matrix-sdk.jsm" -); +const { MatrixSDK } = ChromeUtils.import("resource:///modules/matrix-sdk.jsm"); XPCOMUtils.defineLazyModuleGetters(this, { getMatrixTextForEvent: "resource:///modules/matrixTextForEvent.jsm", }); @@ -45,14 +43,14 @@ */ function getAttachmentUrl(content, homeserverUrl) { if (content.file?.v == "v2") { - return getHttpUriForMxc(homeserverUrl, content.file.url); + return MatrixSDK.getHttpUriForMxc(homeserverUrl, content.file.url); //TODO Actually handle encrypted file contents. } if (!content.url.startsWith("mxc:")) { // Ignore content not served by the homeserver's media repo return ""; } - return getHttpUriForMxc(homeserverUrl, content.url); + return MatrixSDK.getHttpUriForMxc(homeserverUrl, content.url); } /** @@ -215,7 +213,7 @@ if (image.alt) { if (image.src.startsWith("mxc:")) { const link = parsedBody.createElement("a"); - link.href = getHttpUriForMxc(homeserverUrl, image.src); + link.href = MatrixSDK.getHttpUriForMxc(homeserverUrl, image.src); link.textContent = image.alt; if (image.title) { link.title = image.title; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/matrix-sdk.jsm thunderbird-102.10.0+build2/comm/chat/protocols/matrix/matrix-sdk.jsm --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/matrix-sdk.jsm 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/matrix-sdk.jsm 2023-04-11 06:11:52.000000000 +0000 @@ -14,7 +14,7 @@ ); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); -const { Loader, Require, Module } = ChromeUtils.import( +const { Loader, Require } = ChromeUtils.import( "resource://devtools/shared/loader/base-loader.js" ); @@ -22,7 +22,6 @@ const EXPORTED_SYMBOLS = [ "MatrixSDK", - "getHttpUriForMxc", "MatrixCrypto", "SyncState", "OlmLib", @@ -30,14 +29,30 @@ "ReceiptType", ]; +/** + * Set of packages that have a top level index.js. This makes it so we don't + * even try to require them as a js file directly and just fall through to the + * index.js logic. These are paths without matrixPath in front. + * + * @type {Set} + */ +const KNOWN_INDEX_JS = new Set([ + "matrix_events_sdk", + "p_retry", + "retry", + "sdp_transform", + "unhomoglyph", + "matrix_sdk/crypto", + "matrix_sdk/crypto/algorithms", + "matrix_sdk/http_api", + "matrix_sdk/rendezvous", + "matrix_sdk/rendezvous/channels", + "matrix_sdk/rendezvous/transports", + "matrix_widget_api", +]); + // Set-up loading so require works properly in CommonJS modules. -// -// These are organized in a somewhat funky way: -// * First they're ordered by module. -// * Then they're ordered alphabetically by the destination file (e.g. this -// keeps all references to utils.js next to each other). -// * They're then ordered by source, with the bare name first, then prefixed by -// ., then prefixed by .., etc. + let matrixPath = "resource:///modules/matrix/"; let globals = { @@ -45,36 +60,29 @@ btoa, crypto, console, - XMLHttpRequest, + fetch, setTimeout, clearTimeout, setInterval, clearInterval, + AbortController, TextEncoder, TextDecoder, - location: { href: "" }, // workaround for browser-request's is_crossDomain + URL, + URLSearchParams, + IDBKeyRange, + get window() { + return globals; + }, // Necessary for interacting with the logging framework. scriptError, imIDebugMessage: Ci.imIDebugMessage, - URL, - URLSearchParams, - IDBKeyRange, }; let loaderGlobal = { - /** - * We want a minimal window to make sure the SDK stays in non-browser mode - * for the most part. - */ get window() { - return { - crypto, - }; + return globals; }, - /** - * Global should not hold a self-reference to avoid |global.window| from - * being defined, so the SDK doesn't think it's running in a website. - */ get global() { return globals; }, @@ -83,191 +91,98 @@ let loader = Loader({ paths: { // Matrix SDK files. - "": matrixPath + "matrix_sdk/", - matrix: matrixPath + "matrix_sdk/matrix.js", - "../matrix": matrixPath + "matrix_sdk/matrix.js", - "../client": matrixPath + "matrix_sdk/client.js", - "../content-repo": matrixPath + "matrix_sdk/content-repo.js", - "../../errors": matrixPath + "matrix_sdk/errors.js", - "../errors": matrixPath + "matrix_sdk/errors.js", - "../indexeddb-helpers": matrixPath + "matrix_sdk/indexeddb-helpers.js", - "../../indexeddb-helpers": matrixPath + "matrix_sdk/indexeddb-helpers.js", - "../http-api": matrixPath + "matrix_sdk/http-api.js", - "../logger": matrixPath + "matrix_sdk/logger.js", - "../../logger": matrixPath + "matrix_sdk/logger.js", - "../NamespacedValue": matrixPath + "matrix_sdk/NamespacedValue.js", - "../randomstring": matrixPath + "matrix_sdk/randomstring.js", - "../ReEmitter": matrixPath + "matrix_sdk/ReEmitter.js", - "../sync-accumulator": matrixPath + "matrix_sdk/sync-accumulator.js", - "../utils": matrixPath + "matrix_sdk/utils.js", - "../utils.js": matrixPath + "matrix_sdk/utils.js", - "../../utils": matrixPath + "matrix_sdk/utils.js", - - // @types - "@types/auth": matrixPath + "matrix_sdk/types/auth.js", - "@types/beacon": matrixPath + "matrix_sdk/types/beacon.js", - "@types/event": matrixPath + "matrix_sdk/types/event.js", - "../@types/event": matrixPath + "matrix_sdk/types/event.js", - "../../@types/event": matrixPath + "matrix_sdk/types/event.js", - "@types/extensible_events": - matrixPath + "matrix_sdk/types/extensible_events.js", - "@types/location": matrixPath + "matrix_sdk/types/location.js", - "@types/partials": matrixPath + "matrix_sdk/types/partials.js", - "@types/PushRules": matrixPath + "matrix_sdk/types/PushRules.js", - "@types/read_receipts": matrixPath + "matrix_sdk/types/read_receipts.js", - "@types/requests": matrixPath + "empty.js", - "@types/search": matrixPath + "matrix_sdk/types/search.js", - "@types/topic": matrixPath + "matrix_sdk/types/topic.js", - - // crypto - index: matrixPath + "matrix_sdk/crypto/index.js", - "crypto/api": matrixPath + "matrix_sdk/crypto/api.js", - backup: matrixPath + "matrix_sdk/crypto/backup.js", - "crypto/backup": matrixPath + "matrix_sdk/crypto/backup.js", - deviceinfo: matrixPath + "matrix_sdk/crypto/deviceinfo.js", - "../deviceinfo": matrixPath + "matrix_sdk/crypto/deviceinfo.js", - DeviceList: matrixPath + "matrix_sdk/crypto/DeviceList.js", - "../DeviceList": matrixPath + "matrix_sdk/crypto/DeviceList.js", - crypto: matrixPath + "matrix_sdk/crypto/index.js", - olmlib: matrixPath + "matrix_sdk/crypto/olmlib.js", - "../olmlib": matrixPath + "matrix_sdk/crypto/olmlib.js", - "crypto/olmlib": matrixPath + "matrix_sdk/crypto/olmlib.js", - OlmDevice: matrixPath + "matrix_sdk/crypto/OlmDevice.js", - "../OlmDevice": matrixPath + "matrix_sdk/crypto/OlmDevice.js", - "crypto/recoverykey": matrixPath + "matrix_sdk/crypto/recoverykey.js", - recoverykey: matrixPath + "matrix_sdk/crypto/recoverykey.js", - OutgoingRoomKeyRequestManager: - matrixPath + "matrix_sdk/crypto/OutgoingRoomKeyRequestManager.js", - "../OutgoingRoomKeyRequestManager": - matrixPath + "matrix_sdk/crypto/OutgoingRoomKeyRequestManager.js", - "crypto/RoomList": matrixPath + "matrix_sdk/crypto/RoomList.js", - "crypto/CrossSigning": matrixPath + "matrix_sdk/crypto/CrossSigning.js", - CrossSigning: matrixPath + "matrix_sdk/crypto/CrossSigning.js", - EncryptionSetup: matrixPath + "matrix_sdk/crypto/EncryptionSetup.js", - SecretStorage: matrixPath + "matrix_sdk/crypto/SecretStorage.js", - aes: matrixPath + "matrix_sdk/crypto/aes.js", - dehydration: matrixPath + "matrix_sdk/crypto/dehydration.js", - "crypto/dehydration": matrixPath + "matrix_sdk/crypto/dehydration.js", - key_passphrase: matrixPath + "matrix_sdk/crypto/key_passphrase.js", - "crypto/key_passphrase": matrixPath + "matrix_sdk/crypto/key_passphrase.js", - - // crypto/algorithms - base: matrixPath + "matrix_sdk/crypto/algorithms/base.js", - algorithms: matrixPath + "matrix_sdk/crypto/algorithms/index.js", - megolm: matrixPath + "matrix_sdk/crypto/algorithms/megolm.js", - "crypto/algorithms/megolm": - matrixPath + "matrix_sdk/crypto/algorithms/megolm.js", - olm: matrixPath + "matrix_sdk/crypto/algorithms/olm.js", - - // crypto/store - "store/indexeddb-crypto-store": - matrixPath + "matrix_sdk/crypto/store/indexeddb-crypto-store.js", - "crypto/store/indexeddb-crypto-store": - matrixPath + "matrix_sdk/crypto/store/indexeddb-crypto-store.js", - "../crypto/store/indexeddb-crypto-store": - matrixPath + "matrix_sdk/crypto/store/indexeddb-crypto-store.js", - "crypto/store/indexeddb-crypto-store-backend": - matrixPath + "matrix_sdk/crypto/store/indexeddb-crypto-store-backend.js", - "crypto/store/localStorage-crypto-store": - matrixPath + "matrix_sdk/crypto/store/localStorage-crypto-store.js", - "crypto/store/memory-crypto-store": - matrixPath + "matrix_sdk/crypto/store/memory-crypto-store.js", - - // crypto/verification - Base: matrixPath + "matrix_sdk/crypto/verification/Base.js", - Error: matrixPath + "matrix_sdk/crypto/verification/Error.js", - "verification/Base": matrixPath + "matrix_sdk/crypto/verification/Base.js", - "verification/Error": - matrixPath + "matrix_sdk/crypto/verification/Error.js", - "verification/QRCode": - matrixPath + "matrix_sdk/crypto/verification/QRCode.js", - "verification/SAS": matrixPath + "matrix_sdk/crypto/verification/SAS.js", - "crypto/verification/SAS": - matrixPath + "matrix_sdk/crypto/verification/SAS.js", - "verification/IllegalMethod": - matrixPath + "matrix_sdk/crypto/verification/IllegalMethod.js", - - // crypto/verification/request - "verification/request/InRoomChannel": - matrixPath + "matrix_sdk/crypto/verification/request/InRoomChannel.js", - "verification/request/ToDeviceChannel": - matrixPath + "matrix_sdk/crypto/verification/request/ToDeviceChannel.js", - "verification/request/VerificationRequest": - matrixPath + - "matrix_sdk/crypto/verification/request/VerificationRequest.js", - - // models - "../models/event": matrixPath + "matrix_sdk/models/event.js", - "../../models/event": matrixPath + "matrix_sdk/models/event.js", - "../lib/models/event": matrixPath + "matrix_sdk/models/event.js", - "../../lib/models/event": matrixPath + "matrix_sdk/models/event.js", - "../models/room": matrixPath + "matrix_sdk/models/room.js", - "../models/room-member": matrixPath + "matrix_sdk/models/room-member.js", - "../models/typed-event-emitter": - matrixPath + "matrix_sdk/models/typed-event-emitter.js", - "../models/user": matrixPath + "matrix_sdk/models/user.js", + "matrix-sdk": matrixPath + "matrix_sdk", + "matrix-sdk/@types": matrixPath + "matrix_sdk/types", + "matrix-sdk/@types/requests": matrixPath + "empty.js", + "matrix-sdk/http-api": matrixPath + "matrix_sdk/http_api", + "matrix-sdk/rust-crypto": matrixPath + "matrix_sdk/rust_crypto", // Simple (one-file) dependencies. "another-json": matrixPath + "another-json.js", "base-x": matrixPath + "base_x/index.js", - "browser-request": matrixPath + "browser_request/index.js", bs58: matrixPath + "bs58/index.js", "content-type": matrixPath + "content_type/index.js", - qs: matrixPath + "qs.js", // unhomoglyph - unhomoglyph: matrixPath + "unhomoglyph/index.js", - "data.json": matrixPath + "unhomoglyph/data.json", + unhomoglyph: matrixPath + "unhomoglyph", // p-retry - "p-retry": matrixPath + "p_retry/index.js", - retry: matrixPath + "retry/index.js", - "lib/retry": matrixPath + "retry/lib/retry.js", - "lib/retry_operation": matrixPath + "retry/lib/retry_operation.js", + "p-retry": matrixPath + "p_retry", + retry: matrixPath + "retry", // matrix-events-sdk - "matrix-events-sdk": matrixPath + "matrix_events_sdk/index.js", - ExtensibleEvents: matrixPath + "matrix_events_sdk/ExtensibleEvents.js", - InvalidEventError: matrixPath + "matrix_events_sdk/InvalidEventError.js", - IPartialEvent: matrixPath + "empty.js", - types: matrixPath + "matrix_events_sdk/types.js", - NamespacedMap: matrixPath + "matrix_events_sdk/NamespacedMap.js", - "events/EmoteEvent": matrixPath + "matrix_events_sdk/events/EmoteEvent.js", - "events/ExtensibleEvent": - matrixPath + "matrix_events_sdk/events/ExtensibleEvent.js", - "events/MessageEvent": - matrixPath + "matrix_events_sdk/events/MessageEvent.js", - "events/message_types": - matrixPath + "matrix_events_sdk/events/message_types.js", - "events/NoticeEvent": - matrixPath + "matrix_events_sdk/events/NoticeEvent.js", - "events/poll_types": matrixPath + "matrix_events_sdk/events/poll_types.js", - "events/PollEndEvent": - matrixPath + "matrix_events_sdk/events/PollEndEvent.js", - "events/PollResponseEvent": - matrixPath + "matrix_events_sdk/events/PollResponseEvent.js", - "events/PollStartEvent": - matrixPath + "matrix_events_sdk/events/PollStartEvent.js", - "events/relationship_types": - matrixPath + "matrix_events_sdk/events/relationship_types.js", - "interpreters/legacy/MRoomMessage": - matrixPath + "matrix_events_sdk/interpreters/legacy/MRoomMessage.js", - "interpreters/modern/MMessage": - matrixPath + "matrix_events_sdk/interpreters/modern/MMessage.js", - "interpreters/modern/MPoll": - matrixPath + "matrix_events_sdk/interpreters/modern/MPoll.js", - "utility/events": matrixPath + "matrix_events_sdk/utility/events.js", - "utility/MessageMatchers": - matrixPath + "matrix_events_sdk/utility/MessageMatchers.js", + "matrix-events-sdk": matrixPath + "matrix_events_sdk", + "matrix-events-sdk/IPartialEvent": matrixPath + "empty.js", + + // matrix-widget-api + "matrix-widget-api": matrixPath + "matrix_widget_api", + "matrix-widget-api/interfaces/CapabilitiesAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/ContentLoadedAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/ICustomWidgetData": matrixPath + "empty.js", + "matrix-widget-api/interfaces/IJitsiWidgetData": matrixPath + "empty.js", + "matrix-widget-api/interfaces/IRoomEvent": matrixPath + "empty.js", + "matrix-widget-api/interfaces/IStickerpickerWidgetData": + matrixPath + "empty.js", + "matrix-widget-api/interfaces/IWidget": matrixPath + "empty.js", + "matrix-widget-api/interfaces/IWidgetApiRequest": matrixPath + "empty.js", + "matrix-widget-api/interfaces/IWidgetApiResponse": matrixPath + "empty.js", + "matrix-widget-api/interfaces/NavigateAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/OpenIDCredentialsAction": + matrixPath + "empty.js", + "matrix-widget-api/interfaces/ReadEventAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/ReadRelationsAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/ScreenshotAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/SetModalButtonEnabledAction": + matrixPath + "empty.js", + "matrix-widget-api/interfaces/SendAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/SendEventAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/SendToDeviceAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/StickerAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/StickyAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/SupportedVersionsAction": + matrixPath + "empty.js", + "matrix-widget-api/interfaces/TurnServerActions": matrixPath + "empty.js", + "matrix-widget-api/interfaces/VisibilityAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/WidgetAction": matrixPath + "empty.js", + "matrix-widget-api/interfaces/WidgetConfigAction": matrixPath + "empty.js", + "matrix-widget-api/transport/ITransport": matrixPath + "empty.js", + + // sdp-transform + "sdp-transform": matrixPath + "sdp_transform", // Packages that are not included, but an alternate implementation is given. events: matrixPath + "events.js", loglevel: matrixPath + "loglevel.js", "safe-buffer": matrixPath + "safe-buffer.js", - url: matrixPath + "url.js", + uuid: matrixPath + "uuid.js", }, globals: loaderGlobal, sandboxName: "Matrix SDK", + // Custom require hook to support loading */index.js without explicitly + // including it in the require path. + requireHook: (id, require) => { + try { + // Get resolved path without matrixPath prefix and .js extension. + const resolved = require.resolve(id).slice(matrixPath.length, -3); + if (KNOWN_INDEX_JS.has(resolved)) { + throw new Error("Must require index.js for module " + id); + } + return require(id); + } catch (error) { + // Make sure we only try to look for index.js on the initial failure and + // not in requires earlier in the tree. + if (!error.rethrown && !id.endsWith("/index.js")) { + try { + return require(id + "/index.js"); + } catch (indexError) { + indexError.rethrown = true; + throw indexError; + } + } + error.rethrown = true; + throw error; + } + }, }); // Load olm library in a browser-like environment. This allows it to load its @@ -299,25 +214,18 @@ loader.globals.Olm = olmScope.Olm; globals.Olm = olmScope.Olm; -let requirer = Module("matrix-module", ""); -let require = Require(loader, requirer); +let require = Require(loader, { id: "matrix-module" }); // Load the buffer shim into the global commonJS scope loader.globals.Buffer = require("safe-buffer").Buffer; globals.Buffer = loader.globals.Buffer; // The main entry point into the Matrix client. -let MatrixSDK = require("browser-index.js"); - -// Helper functions. -let getHttpUriForMxc = require("../content-repo").getHttpUriForMxc; - -let MatrixCrypto = require("./crypto"); - -let { SyncState } = require("./sync"); - -let OlmLib = require("./crypto/olmlib"); - -let { SasEvent } = require("./crypto/verification/SAS"); +let MatrixSDK = require("matrix-sdk/browser-index.js"); -let { ReceiptType } = require("@types/read_receipts"); +// Helper enums not exposed on MatrixSDK. +let MatrixCrypto = require("matrix-sdk/crypto"); +let { SyncState } = require("matrix-sdk/sync"); +let OlmLib = require("matrix-sdk/crypto/olmlib"); +let { SasEvent } = require("matrix-sdk/crypto/verification/SAS"); +let { ReceiptType } = require("matrix-sdk/@types/read_receipts"); diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/shims/moz.build thunderbird-102.10.0+build2/comm/chat/protocols/matrix/shims/moz.build --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/shims/moz.build 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/shims/moz.build 2023-04-11 06:11:52.000000000 +0000 @@ -10,5 +10,5 @@ "empty.js", "loglevel.js", "safe-buffer.js", - "url.js", + "uuid.js", ] diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/shims/url.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/shims/url.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/shims/url.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/shims/url.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -/* globals module */ - -function parse(u) { - return new URL(u); -} - -module.exports = { - parse, -}; diff -Nru thunderbird-102.9.0+build1/comm/chat/protocols/matrix/shims/uuid.js thunderbird-102.10.0+build2/comm/chat/protocols/matrix/shims/uuid.js --- thunderbird-102.9.0+build1/comm/chat/protocols/matrix/shims/uuid.js 1970-01-01 00:00:00.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/chat/protocols/matrix/shims/uuid.js 2023-04-11 06:11:52.000000000 +0000 @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* globals module */ + +const v4 = () => crypto.randomUUID(); + +module.exports = { + v4, +}; diff -Nru thunderbird-102.9.0+build1/comm/.gecko_rev.yml thunderbird-102.10.0+build2/comm/.gecko_rev.yml --- thunderbird-102.9.0+build1/comm/.gecko_rev.yml 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/.gecko_rev.yml 2023-04-11 06:11:52.000000000 +0000 @@ -1,8 +1,8 @@ --- GECKO_BASE_REPOSITORY: https://hg.mozilla.org/mozilla-unified GECKO_HEAD_REPOSITORY: https://hg.mozilla.org/releases/mozilla-esr102 -GECKO_HEAD_REF: FIREFOX_102_9_0esr_BUILD2 -GECKO_HEAD_REV: e26ff04290d095dac006a3710b07077ee5d20f31 +GECKO_HEAD_REF: FIREFOX_102_10_0esr_BUILD1 +GECKO_HEAD_REV: 737a5c36e0f939b688ff1d6fb75b139cfdf60ae9 ### For comm-central # GECKO_BASE_REPOSITORY: https://hg.mozilla.org/mozilla-unified diff -Nru thunderbird-102.9.0+build1/comm/mail/base/content/overrides/app-license-body.html thunderbird-102.10.0+build2/comm/mail/base/content/overrides/app-license-body.html --- thunderbird-102.9.0+build1/comm/mail/base/content/overrides/app-license-body.html 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/base/content/overrides/app-license-body.html 2023-04-11 06:11:52.000000000 +0000 @@ -4,6 +4,198 @@
+

Apache License 2.0

+ +

This license applies to the following files: +

    +
  • chat/protocols/matrix/lib/@matrix-org/olm
  • +
  • chat/protocols/matrix/lib/another-json
  • +
  • chat/protocols/matrix/lib/matrix-events-sdk
  • +
  • chat/protocols/matrix/lib/matrix-sdk
  • +
  • chat/protocols/matrix/lib/matrix-widget-api
  • +
+

+ +
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+

BSD-3-Clause License

This license applies to the following files: @@ -17,9 +209,9 @@

-
-  Copyright (c) 2012, Intel Corporation
+    See the individual LICENSE files for copyright owners.
 
+
   All rights reserved.
 
   Redistribution and use in source and binary forms, with or without
@@ -693,7 +885,7 @@
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     
-

getopt.c License

+

getopt.c License

This license applies to the following files:

  • third_party/niwcompat/getopt.c
  • @@ -727,3 +919,305 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ +

base-x License

+

This license applies to the following files:

+
    +
  • chat/protocols/matrix/lib/base-x
  • +
+
+The MIT License (MIT)
+
+Copyright (c) 2018 base-x contributors
+Copyright (c) 2014-2018 The Bitcoin Core developers
+
+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.
+    
+ + +

bs58 License

+

This license applies to the following files:

+
    +
  • chat/protocols/matrix/lib/bs58
  • +
+
+MIT License
+
+Copyright (c) 2018 cryptocoinjs
+
+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.
+    
+ + +

content-type License

+

This license applies to the following files:

+
    +
  • chat/protocols/matrix/lib/content-type
  • +
+
+(The MIT License)
+
+Copyright (c) 2015 Douglas Christopher Wilson
+
+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.
+    
+ + +

events License

+

This license applies to the following files:

+
    +
  • chat/protocols/matrix/lib/events
  • +
+
+MIT
+
+Copyright Joyent, Inc. and other Node contributors.
+
+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.
+    
+ + +

p-retry License

+

This license applies to the following files:

+
    +
  • chat/protocols/matrix/lib/p-retry
  • +
+
+MIT License
+
+Copyright (c) Sindre Sorhus  (sindresorhus.com)
+
+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.
+    
+ +

retry License

+

This license applies to the following files:

+
    +
  • chat/protocols/matrix/lib/retry
  • +
+
+Copyright (c) 2011:
+Tim Koschützki (tim@debuggable.com)
+Felix Geisendörfer (felix@debuggable.com)
+
+ 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.
+    
+ +

unhomoglyph License

+

This license applies to the following files:

+
    +
  • chat/protocols/matrix/lib/unhomoglyph
  • +
+
+Copyright (c) 2016 Vitaly Puzrin.
+
+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.
+    
+ +

sax License

+

This license applies to the following files:

+
    +
  • chat/protocols/xmpp/lib/sax
  • +
+
+The ISC License
+
+Copyright (c) Isaac Z. Schlueter and Contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+====
+
+`String.fromCodePoint` by Mathias Bynens used according to terms of MIT
+License, as follows:
+
+    Copyright Mathias Bynens 
+
+    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.
+    
+ +

GNU Lesser General Public License 2.1

+

This product contains code from the following LGPLed libraries:

+ + + (These libraries only ship in some versions of this product.) + Read the license above. + +

sdp-transform License

+

This license applies to the following files:

+
    +
  • chat/protocols/matrix/lib/sdp-transform
  • +
+
+(The MIT License)
+
+Copyright (c) 2013 Eirik Albrigtsen
+
+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 -Nru thunderbird-102.9.0+build1/comm/mail/base/content/overrides/app-license-list.html thunderbird-102.10.0+build2/comm/mail/base/content/overrides/app-license-list.html --- thunderbird-102.9.0+build1/comm/mail/base/content/overrides/app-license-list.html 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/base/content/overrides/app-license-list.html 2023-04-11 06:11:52.000000000 +0000 @@ -5,6 +5,7 @@
diff -Nru thunderbird-102.9.0+build1/comm/mail/components/compose/content/MsgComposeCommands.js thunderbird-102.10.0+build2/comm/mail/components/compose/content/MsgComposeCommands.js --- thunderbird-102.9.0+build1/comm/mail/components/compose/content/MsgComposeCommands.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/components/compose/content/MsgComposeCommands.js 2023-04-11 06:11:52.000000000 +0000 @@ -170,7 +170,8 @@ // gSMFields separate allows switching as needed. var gSMFields = null; -var gSMCertsMap = new Map(); +var gSMPendingCertLookupSet = new Set(); +var gSMCertsAlreadyLookedUpInLDAP = new Set(); var gSelectedTechnologyIsPGP = false; @@ -316,13 +317,13 @@ }; const keyObserver = { - observe: (subject, topic, data) => { + observe: async (subject, topic, data) => { switch (topic) { case "openpgp-key-change": EnigmailKeyRing.clearCache(); // fall through case "openpgp-acceptance-change": - checkRecipientKeys(); + await checkRecipientKeys(); gKeyAssistant.onExternalKeyChange(); break; default: @@ -1865,6 +1866,7 @@ isEncryptionCertAvailable: gCurrentIdentity.getUnicharAttribute("encryption_cert_name") != "", currentIdentity: gCurrentIdentity, + recipients: getEncryptionCompatibleRecipients(), } ); } @@ -3107,7 +3109,7 @@ gLoadingComplete = true; // Automatic checking is disabled while composer is loading, // perform the check for encryption status now. - checkRecipientKeys(); + checkRecipientKeysAndCerts(); } // checks if the passed in string is a mailto url, if it is, generates nsIMsgComposeParams @@ -3359,6 +3361,11 @@ }); } +async function checkRecipientKeysAndCerts() { + await checkRecipientKeys(); + checkRecipientCerts(); +} + async function checkRecipientKeys() { if (!gLoadingComplete) { // Let's not do this while we're still loading the composer window, @@ -3369,13 +3376,10 @@ return; } - let remindSMime = Services.prefs.getBoolPref( - "mail.smime.remind_encryption_possible" - ); let remindOpenPGP = Services.prefs.getBoolPref( "mail.openpgp.remind_encryption_possible" ); - if (!remindSMime && !remindOpenPGP) { + if (!remindOpenPGP) { return; } @@ -3386,9 +3390,6 @@ // undecided key, or the 1st expired key, or the 1st rejected key, in this // order. - let emailsWithMissingCerts = []; - let haveAllCerts = false; - let emailsWithMissingKeys = []; let haveAllKeys = false; @@ -3412,76 +3413,174 @@ } } - if (remindSMime && (gSendEncrypted || isSmimeEncryptionConfigured())) { - let compFields = Cc[ - "@mozilla.org/messengercompose/composefields;1" - ].createInstance(Ci.nsIMsgCompFields); - Recipients2CompFields(compFields); - let helper = Cc[ - "@mozilla.org/messenger-smime/smimejshelper;1" - ].createInstance(Ci.nsISMimeJSHelper); - - let outEmailAddresses = {}; - - helper.getRecipients(compFields, outEmailAddresses); + if (!gSendEncrypted) { + if (recipients.length && haveAllKeys) { + updateEncryptionTechReminder("OpenPGP"); + } else { + updateEncryptionTechReminder(null); + } - for (let i = 0; i < outEmailAddresses.value.length; i++) { - let email = outEmailAddresses.value[i]; + updateKeyNotifications([]); + return; + } - let certFromCache = gSMCertsMap.get(email); - if (certFromCache) { - continue; - } + if (gSelectedTechnologyIsPGP) { + updateEncryptionTechReminder(null); + updateKeyNotifications(emailsWithMissingKeys); + } +} - let outCertIssuedInfo = {}; - let outCertExpiresInfo = {}; - let outCert = {}; - - helper.getValidCertInfo( - email, - outCertIssuedInfo, - outCertExpiresInfo, - outCert - ); +/** + * S/MIME. Callers cannot wait for this to complete. Cert verification + * involves OCSP, which must run on a background thread. + * Calling this function will result in a delayed UI update as soon + * as necessary background activity has been completed. Calling this + * function multiple times pending OCSP is safe, because we're tracking + * pending requests. As soon as all are done, processing will continue + * by calling continueCheckRecipientCerts(). + */ +function checkRecipientCerts() { + async function continueCheckRecipientCerts() { + if (!Services.prefs.getBoolPref("mail.smime.remind_encryption_possible")) { + // No UI updates necessary, our processing did the necessary + // filling of the OCSP cache. + return; + } - if (outCert.value) { - gSMCertsMap.set(email, outCert.value); + let recipients = getEncryptionCompatibleRecipients(); + let emailsWithMissingCerts = recipients.filter( + email => !gSMFields.haveValidCertForEmail(email) + ); + let haveAllCerts = !emailsWithMissingCerts.length; + + if (!gSendEncrypted) { + if (recipients.length && haveAllCerts) { + updateEncryptionTechReminder("SMIME"); } else { - emailsWithMissingCerts.push(email); - continue; + updateEncryptionTechReminder(null); } + + updateKeyNotifications([]); + return; } - if (!emailsWithMissingCerts.length) { - haveAllCerts = true; + if (!gSelectedTechnologyIsPGP) { + updateEncryptionTechReminder(null); + updateKeyNotifications(emailsWithMissingCerts); } } - if (!gSendEncrypted) { - if (recipients.length && (haveAllCerts || haveAllKeys)) { - await updateEncryptionReminder(haveAllKeys, haveAllCerts); - } else { - await updateEncryptionReminder(false, false); - } + if (!gLoadingComplete) { + // Let's not do this while we're still loading the composer window, + // it can have side effects, see bug 1777683. + // Also, if multiple recipients are added to an email automatically + // e.g. during reply-all, it doesn't make sense to execute this + // function every time after one of them gets added. + return; + } - updateKeyNotifications([]); + if (!isSmimeEncryptionConfigured()) { return; } - await updateEncryptionReminder(false, false); - updateKeyNotifications( - gSelectedTechnologyIsPGP ? emailsWithMissingKeys : emailsWithMissingCerts - ); + let recipients = getEncryptionCompatibleRecipients(); + // Calculate key notifications. + // 1 notification at most per email address that has no valid key. + // If an email address has several invalid keys, we notify only about the 1st + // undecided key, or the 1st expired key, or the 1st rejected key, in this + // order. + + /** @implements {nsIDoneFindCertForEmailCallback} */ + let doneFindCertForEmailCallback = { + QueryInterface: ChromeUtils.generateQI(["nsIDoneFindCertForEmailCallback"]), + + findCertDone(email, cert) { + let isStaleResult = !gSMPendingCertLookupSet.has(email); + // isStaleResult true means, this recipient was removed by the + // user while we were looking for the cert in the background. + // Let's remember the result, but don't trigger any actions + // based on it. + + if (cert) { + gSMFields.cacheValidCertForEmail(email, cert ? cert.dbKey : ""); + } + if (isStaleResult) { + return; + } + gSMPendingCertLookupSet.delete(email); + if (!cert && !gSMCertsAlreadyLookedUpInLDAP.has(email)) { + let autocompleteLdap = Services.prefs.getBoolPref( + "ldap_2.autoComplete.useDirectory" + ); + + if (autocompleteLdap) { + gSMCertsAlreadyLookedUpInLDAP.add(email); + + let autocompleteDirectory = null; + if (gCurrentIdentity.overrideGlobalPref) { + autocompleteDirectory = gCurrentIdentity.directoryServer; + } else { + autocompleteDirectory = Services.prefs.getCharPref( + "ldap_2.autoComplete.directoryServer" + ); + } + + if (autocompleteDirectory) { + window.openDialog( + "chrome://messenger-smime/content/certFetchingStatus.xhtml", + "", + "chrome,resizable=1,modal=1,dialog=1", + autocompleteDirectory, + [email] + ); + } + + gSMPendingCertLookupSet.add(email); + gSMFields.asyncFindCertByEmailAddr( + email, + doneFindCertForEmailCallback + ); + } + } + + if (gSMPendingCertLookupSet.size) { + // must continue to wait for more calls to this function + return; + } + + // No more lookups pending. + continueCheckRecipientCerts(); + }, + }; + + for (let email of recipients) { + if (gSMFields.haveValidCertForEmail(email)) { + continue; + } + + if (gSMPendingCertLookupSet.has(email)) { + // lookup already currently running for this email + continue; + } + + gSMPendingCertLookupSet.add(email); + gSMFields.asyncFindCertByEmailAddr(email, doneFindCertForEmailCallback); + } + + if (!gSMPendingCertLookupSet.size) { + // immediately continue + continueCheckRecipientCerts(); + } } /** * Display (or hide) the notification that informs the user that * encryption is possible (but currently not enabled). * - * @param {boolean} canEnableOpenPGP - If OpenPGP encryption is possible - * @param {boolean} canEnableSMIME - If S/MIME encryption is possible + * @param {string} technology - The technology that is possible, + * ("OpenPGP" or "SMIME"), or null if none is possible. */ -async function updateEncryptionReminder(canEnableOpenPGP, canEnableSMIME) { +function updateEncryptionTechReminder(technology) { let enableNotification = gComposeNotification.getNotificationWithValue( "enableNotification" ); @@ -3489,13 +3588,14 @@ gComposeNotification.removeNotification(enableNotification); } - if (!canEnableOpenPGP && !canEnableSMIME) { + if (!technology || (technology != "OpenPGP" && technology != "SMIME")) { return; } - let labelId = canEnableOpenPGP - ? "can-encrypt-openpgp-notification" - : "can-encrypt-smime-notification"; + let labelId = + technology == "OpenPGP" + ? "can-encrypt-openpgp-notification" + : "can-encrypt-smime-notification"; gComposeNotification.appendNotification( "enableNotification", @@ -3507,10 +3607,14 @@ { "l10n-id": "can-e2e-encrypt-button", callback() { - gSelectedTechnologyIsPGP = canEnableOpenPGP; + gSelectedTechnologyIsPGP = technology == "OpenPGP"; updateE2eeOptions(true); updateEncryptionOptions(); - checkRecipientKeys(); + if (technology == "OpenPGP") { + checkRecipientKeys(); + } else { + checkRecipientCerts(); + } return true; }, }, @@ -4741,7 +4845,7 @@ }); window.addEventListener("sendencryptedchange", checkEncryptedBccRecipients); - gRecipientKeysObserver = new MutationObserver(checkRecipientKeys); + gRecipientKeysObserver = new MutationObserver(checkRecipientKeysAndCerts); gRecipientKeysObserver.observe(document.getElementById("toAddrContainer"), { childList: true, }); @@ -4751,7 +4855,7 @@ gRecipientKeysObserver.observe(document.getElementById("bccAddrContainer"), { childList: true, }); - window.addEventListener("sendencryptedchange", checkRecipientKeys); + window.addEventListener("sendencryptedchange", checkRecipientKeysAndCerts); } /* eslint-enable complexity */ @@ -4871,7 +4975,14 @@ // For identities without any e2ee setup, we want a good default // technology selection. Avoid a technology that isn't configured // anywhere. - gSelectedTechnologyIsPGP = isOpenPGPAvailable; + + if (configuredOpenPGP) { + gSelectedTechnologyIsPGP = true; + } else if (configuredSMIME) { + gSelectedTechnologyIsPGP = false; + } else { + gSelectedTechnologyIsPGP = isOpenPGPAvailable; + } if (configuredOpenPGP) { if (!configuredSMIME) { @@ -5363,7 +5474,7 @@ gSelectedTechnologyIsPGP = false; updateEncryptionOptions(); updateEncryptedSubject(); - checkRecipientKeys(); + checkRecipientCerts(); } break; @@ -5508,9 +5619,11 @@ return; } - emailAddresses = Cc["@mozilla.org/messenger-smime/smimejshelper;1"] - .createInstance(Ci.nsISMimeJSHelper) - .getNoCertAddresses(gMsgCompose.compFields); + for (let email of getEncryptionCompatibleRecipients()) { + if (!gSMFields.haveValidCertForEmail(email)) { + emailAddresses.push(email); + } + } } catch (e) { return; } diff -Nru thunderbird-102.9.0+build1/comm/mail/components/extensions/ExtensionPopups.jsm thunderbird-102.10.0+build2/comm/mail/components/extensions/ExtensionPopups.jsm --- thunderbird-102.9.0+build1/comm/mail/components/extensions/ExtensionPopups.jsm 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/components/extensions/ExtensionPopups.jsm 2023-04-11 06:11:52.000000000 +0000 @@ -441,10 +441,15 @@ return panel; }; - // Create a temporary panel to hold the browser while it pre-loads its - // content. This panel will never be shown, but the browser's docShell will - // be swapped with the browser in the real panel when it's ready. For remote - // extensions, this popup is shared between all extensions. + // Firefox creates a temporary panel to hold the browser while it pre-loads + // its content (starting on mouseover already). This panel will never be shown, + // but the browser's docShell will be swapped with the browser in the real + // panel when it's ready (in ViewPopup.attach()). + // For remote extensions, Firefox shares this temporary panel between all + // extensions. + + // NOTE: Thunderbird currently does not pre-load the popup and really uses + // the "temporary" panel when displaying the popup to the user. let panel; if (extension.remote) { panel = document.getElementById(REMOTE_PANEL_ID); @@ -472,6 +477,8 @@ * Attaches the pre-loaded browser to the given view node, and reserves a * promise which resolves when the browser is ready. * + * NOTE: Not used by Thunderbird. + * * @param {Element} viewNode * The node to attach the browser to. * @returns {Promise} @@ -602,9 +609,15 @@ removeTempPanel() { if (this.tempPanel) { - if (this.tempPanel.id !== REMOTE_PANEL_ID) { - this.tempPanel.remove(); - } + // NOTE: Thunderbird currently does not pre-load the popup into a temporary + // panel as Firefox is doing it. We therefore do not have to "save" + // the temporary panel for later re-use, but really have to remove it. + // See Bug 1451058 for why Firefox uses the following conditional + // remove(). + + // if (this.tempPanel.id !== REMOTE_PANEL_ID) { + this.tempPanel.remove(); + // } this.tempPanel = null; } if (this.tempBrowser) { diff -Nru thunderbird-102.9.0+build1/comm/mail/components/MessengerContentHandler.jsm thunderbird-102.10.0+build2/comm/mail/components/MessengerContentHandler.jsm --- thunderbird-102.9.0+build1/comm/mail/components/MessengerContentHandler.jsm 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/components/MessengerContentHandler.jsm 2023-04-11 06:11:52.000000000 +0000 @@ -398,6 +398,11 @@ } } if (uri) { + if (/^file:/i.test(uri)) { + // Turn file URL into a file path so `resolveFile()` will work. + let fileURL = cmdLine.resolveURI(uri); + uri = fileURL.QueryInterface(Ci.nsIFileURL).file.path; + } // Check for protocols first then look at the file ending. // Protocols are able to contain file endings like '.ics'. if (/^https?:/i.test(uri)) { diff -Nru thunderbird-102.9.0+build1/comm/mail/config/version_display.txt thunderbird-102.10.0+build2/comm/mail/config/version_display.txt --- thunderbird-102.9.0+build1/comm/mail/config/version_display.txt 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/config/version_display.txt 2023-04-11 06:11:52.000000000 +0000 @@ -1 +1 @@ -102.9.0 +102.10.0 diff -Nru thunderbird-102.9.0+build1/comm/mail/config/version.txt thunderbird-102.10.0+build2/comm/mail/config/version.txt --- thunderbird-102.9.0+build1/comm/mail/config/version.txt 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/config/version.txt 2023-04-11 06:11:52.000000000 +0000 @@ -1 +1 @@ -102.9.0 +102.10.0 diff -Nru thunderbird-102.9.0+build1/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js thunderbird-102.10.0+build2/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js --- thunderbird-102.9.0+build1/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/extensions/openpgp/content/ui/enigmailMsgComposeOverlay.js 2023-04-11 06:11:52.000000000 +0000 @@ -991,61 +991,7 @@ no return value */ - processFinalState(sendFlags) { - /* - EnigmailLog.DEBUG("enigmailMsgComposeOverlay.js: Enigmail.msg.processFinalState()\n"); - - const SIGN = EnigmailConstants.SEND_SIGNED; - const ENCRYPT = EnigmailConstants.SEND_ENCRYPTED; - - - let encReason = ""; - let signReason = ""; - let pgpEnabled = Enigmail.msg.wasEnigmailEnabledForIdentity(); - let smimeEnabled = Enigmail.msg.isSmimeEnabled(); - - // ------ 1. process OpenPGP status ------ - - //pgpEnabled - //smimeEnabled - - - - // ------ 2. Process S/MIME status ------ - if (gSMFields) { - - //gSMFields.requireEncryptMessage = false; - //gSMFields.signMessage = false; - - if (!encryptSmime) { - if (autoSendEncrypted === 1) { - if (this.isSmimeEncryptionPossible()) { - if (this.mimePreferOpenPGP === 0) { - // S/MIME is preferred and encryption is possible - encryptSmime = true; - } - } - } - } - //gSMFields.requireEncryptMessage = true; - //gSMFields.signMessage = true; - - // smime policy - //if (this.identity.encryptionPolicy > 0) - - // update the S/MIME GUI elements - try { - setSecuritySettings("1"); - } - catch (ex) {} - - try { - setSecuritySettings("2"); - } - catch (ex) {} - } - */ - }, + processFinalState(sendFlags) {}, /* check if encryption is possible (have keys for everyone or not) */ @@ -1227,30 +1173,6 @@ }, */ - /** - * check if S/MIME encryption can be enabled - * - * @return: Boolean - true: keys for all recipients are available - */ - isSmimeEncryptionPossible() { - if (!gCurrentIdentity.getUnicharAttribute("encryption_cert_name")) { - return false; - } - - // Enable encryption if keys for all recipients are available. - try { - if (!gMsgCompose.compFields.hasRecipients) { - return false; - } - let addresses = Cc["@mozilla.org/messenger-smime/smimejshelper;1"] - .createInstance(Ci.nsISMimeJSHelper) - .getNoCertAddresses(gMsgCompose.compFields); - return addresses.length == 0; - } catch (e) { - return false; - } - }, - /* Manage the wrapping of inline signed mails * * @wrapresultObj: Result: diff -Nru thunderbird-102.9.0+build1/comm/mail/themes/linux/mail/compose/messengercompose.css thunderbird-102.10.0+build2/comm/mail/themes/linux/mail/compose/messengercompose.css --- thunderbird-102.9.0+build1/comm/mail/themes/linux/mail/compose/messengercompose.css 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/themes/linux/mail/compose/messengercompose.css 2023-04-11 06:11:52.000000000 +0000 @@ -132,26 +132,6 @@ margin-inline-start: 3px; } -/* ..... fg/bg color picker ..... */ - -.color-button { - border: 1px inset ThreeDFace; - padding: 0; - width: 14px; - height: 12px; - margin: 2px; -} - -.color-button:hover { - border: 1px solid ThreeDDarkShadow; -} - -.color-button[disabled="true"], -.color-button[disabled="true"]:hover { - border: 1px inset ThreeDFace; - opacity: 0.5; -} - /* ::::: address book sidebar ::::: */ #contactsBrowser { diff -Nru thunderbird-102.9.0+build1/comm/mail/themes/osx/mail/compose/messengercompose.css thunderbird-102.10.0+build2/comm/mail/themes/osx/mail/compose/messengercompose.css --- thunderbird-102.9.0+build1/comm/mail/themes/osx/mail/compose/messengercompose.css 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/themes/osx/mail/compose/messengercompose.css 2023-04-11 06:11:52.000000000 +0000 @@ -179,27 +179,6 @@ max-width: 15em; } -#ColorButtons { - margin-block: 0; - margin-inline: 3px 5px; -} - -/* ..... fg/bg color picker ..... */ - -.color-button { - border: 1px solid #a0a0a0; - padding: 0; - width: 20px; - height: 13px; - margin: 2px; -} - -.color-button[disabled="true"], -.color-button[disabled="true"]:hover { - border: 1px inset #a0a0a0; - opacity: 0.5; -} - /* ::::: address book sidebar ::::: */ #contactsBrowserTitle { diff -Nru thunderbird-102.9.0+build1/comm/mail/themes/shared/mail/messengercompose.css thunderbird-102.10.0+build2/comm/mail/themes/shared/mail/messengercompose.css --- thunderbird-102.9.0+build1/comm/mail/themes/shared/mail/messengercompose.css 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/themes/shared/mail/messengercompose.css 2023-04-11 06:11:52.000000000 +0000 @@ -665,6 +665,22 @@ /* ..... fg/bg color picker ..... */ +#ColorButtons { + margin-inline: 3px 4px; +} + +.color-button { + border: 1px solid var(--toolbarbutton-active-bordercolor); + padding: 0; + width: 18px; + height: 15px; + margin: 2px; +} + +.color-button[disabled="true"] { + opacity: 0.5; +} + .ColorPickerLabel { border: 1px inset ThreeDFace; margin: 0; diff -Nru thunderbird-102.9.0+build1/comm/mail/themes/windows/mail/compose/messengercompose.css thunderbird-102.10.0+build2/comm/mail/themes/windows/mail/compose/messengercompose.css --- thunderbird-102.9.0+build1/comm/mail/themes/windows/mail/compose/messengercompose.css 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mail/themes/windows/mail/compose/messengercompose.css 2023-04-11 06:11:52.000000000 +0000 @@ -162,26 +162,6 @@ margin: 1px; } -/* ..... fg/bg color picker ..... */ - -.color-button { - border: 1px inset ThreeDFace; - padding: 0; - width: 14px; - height: 12px; - margin: 2px; -} - -.color-button:hover { - border: 1px solid ThreeDDarkShadow; -} - -.color-button[disabled="true"], -.color-button[disabled="true"]:hover { - border: 1px inset ThreeDFace; - opacity: 0.5; -} - /* ::::: address book sidebar ::::: */ #compose-toolbox { diff -Nru thunderbird-102.9.0+build1/comm/mailnews/base/src/nsNewMailnewsURI.cpp thunderbird-102.10.0+build2/comm/mailnews/base/src/nsNewMailnewsURI.cpp --- thunderbird-102.9.0+build1/comm/mailnews/base/src/nsNewMailnewsURI.cpp 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mailnews/base/src/nsNewMailnewsURI.cpp 2023-04-11 06:11:53.000000000 +0000 @@ -42,16 +42,25 @@ if (NS_FAILED(rv)) return rv; } + // Creating IMAP/mailbox URIs off the main thread can lead to crashes. + // Seems to happen when viewing PDFs. if (scheme.EqualsLiteral("mailbox") || scheme.EqualsLiteral("mailbox-message")) { - return nsMailboxService::NewURI(aSpec, aCharset, aBaseURI, aURI); + if (NS_IsMainThread()) { + return nsMailboxService::NewURI(aSpec, aCharset, aBaseURI, aURI); + } + auto NewURI = [&aSpec, &aCharset, &aBaseURI, aURI, &rv ]() -> auto{ + rv = nsMailboxService::NewURI(aSpec, aCharset, aBaseURI, aURI); + }; + nsCOMPtr task = NS_NewRunnableFunction("NewURI", NewURI); + mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), task); + return rv; } if (scheme.EqualsLiteral("imap") || scheme.EqualsLiteral("imap-message")) { if (NS_IsMainThread()) { return nsImapService::NewURI(aSpec, aCharset, aBaseURI, aURI); } - // Creating IMAP URIs off the main thread can lead to crashes. - // Seems to happen when viewing PDFs. auto NewURI = [&aSpec, &aCharset, &aBaseURI, aURI, &rv ]() -> auto{ rv = nsImapService::NewURI(aSpec, aCharset, aBaseURI, aURI); }; diff -Nru thunderbird-102.9.0+build1/comm/mailnews/base/src/OAuth2.jsm thunderbird-102.10.0+build2/comm/mailnews/base/src/OAuth2.jsm --- thunderbird-102.9.0+build1/comm/mailnews/base/src/OAuth2.jsm 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mailnews/base/src/OAuth2.jsm 2023-04-11 06:11:52.000000000 +0000 @@ -167,7 +167,8 @@ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { const wpl = Ci.nsIWebProgressListener; if (aStateFlags & (wpl.STATE_START | wpl.STATE_IS_NETWORK)) { - this._checkForRedirect(aRequest.name); + let channel = aRequest.QueryInterface(Ci.nsIChannel); + this._checkForRedirect(channel.URI.spec); } }, onLocationChange(aWebProgress, aRequest, aLocation) { diff -Nru thunderbird-102.9.0+build1/comm/mailnews/build/nsMailModule.cpp thunderbird-102.10.0+build2/comm/mailnews/build/nsMailModule.cpp --- thunderbird-102.9.0+build1/comm/mailnews/build/nsMailModule.cpp 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mailnews/build/nsMailModule.cpp 2023-04-11 06:11:52.000000000 +0000 @@ -242,7 +242,6 @@ #include "nsCertPicker.h" #include "nsMsgSMIMECID.h" #include "nsMsgComposeSecure.h" -#include "nsSMimeJSHelper.h" #include "nsEncryptedSMIMEURIsService.h" /////////////////////////////////////////////////////////////////////////////// @@ -595,7 +594,6 @@ // smime factories //////////////////////////////////////////////////////////////////////////////// NS_GENERIC_FACTORY_CONSTRUCTOR(nsMsgComposeSecure) -NS_GENERIC_FACTORY_CONSTRUCTOR(nsSMimeJSHelper) NS_GENERIC_FACTORY_CONSTRUCTOR(nsEncryptedSMIMEURIsService) NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsCMSDecoder, Init) NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsCMSDecoderJS, Init) @@ -605,7 +603,6 @@ NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsCertPicker, Init) NS_DEFINE_NAMED_CID(NS_MSGCOMPOSESECURE_CID); -NS_DEFINE_NAMED_CID(NS_SMIMEJSJELPER_CID); NS_DEFINE_NAMED_CID(NS_SMIMEENCRYPTURISERVICE_CID); NS_DEFINE_NAMED_CID(NS_CMSDECODER_CID); NS_DEFINE_NAMED_CID(NS_CMSDECODERJS_CID); @@ -821,7 +818,6 @@ {&kNS_MSGMDNGENERATOR_CID, false, NULL, nsMsgMdnGeneratorConstructor}, // SMime Entries {&kNS_MSGCOMPOSESECURE_CID, false, NULL, nsMsgComposeSecureConstructor}, - {&kNS_SMIMEJSJELPER_CID, false, NULL, nsSMimeJSHelperConstructor}, {&kNS_SMIMEENCRYPTURISERVICE_CID, false, NULL, nsEncryptedSMIMEURIsServiceConstructor}, {&kNS_CMSDECODER_CID, false, NULL, nsCMSDecoderConstructor}, @@ -1016,7 +1012,6 @@ {NS_MSGMDNGENERATOR_CONTRACTID, &kNS_MSGMDNGENERATOR_CID}, // SMime Entries {NS_MSGCOMPOSESECURE_CONTRACTID, &kNS_MSGCOMPOSESECURE_CID}, - {NS_SMIMEJSHELPER_CONTRACTID, &kNS_SMIMEJSJELPER_CID}, {NS_SMIMEENCRYPTURISERVICE_CONTRACTID, &kNS_SMIMEENCRYPTURISERVICE_CID}, {NS_CMSSECUREMESSAGE_CONTRACTID, &kNS_CMSSECUREMESSAGE_CID}, {NS_CMSDECODER_CONTRACTID, &kNS_CMSDECODER_CID}, diff -Nru thunderbird-102.9.0+build1/comm/mailnews/compose/public/nsIMsgComposeSecure.idl thunderbird-102.10.0+build2/comm/mailnews/compose/public/nsIMsgComposeSecure.idl --- thunderbird-102.9.0+build1/comm/mailnews/compose/public/nsIMsgComposeSecure.idl 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mailnews/compose/public/nsIMsgComposeSecure.idl 2023-04-11 06:11:53.000000000 +0000 @@ -12,7 +12,30 @@ interface nsIOutputStream; interface nsIX509Cert; -/* Security interface */ +/** + * Callback type for use with asyncFindCertByEmailAddr. + */ +[scriptable, function, uuid(6149d7d3-14bf-4280-8451-60fb48263894)] +interface nsIDoneFindCertForEmailCallback : nsISupports { + /** + * Called after a searching for a certificate is done. + * + * @param emailAddress - The email address that was used as the key + * to find this certificate. + * @param cert - The valid certificate that was found, + * or null, if no valid cert was found. + */ + void findCertDone(in AUTF8String emailAddress, + in nsIX509Cert cert); +}; + +/** + * An instance of this type is related to exactly one email message + * while the user is composing it. + * Besides remembering flags and providing helper code, it is used to + * cache information about valid S/MIME encryption certificates that + * were found and which may be used at send time. + */ [scriptable, uuid(245f2adc-410e-4bdb-91e2-a7bb42d61787)] interface nsIMsgComposeSecure : nsISupports { @@ -73,21 +96,50 @@ void finishCryptoEncapsulation(in boolean aAbort, in nsIMsgSendReport sendReport); /** - * Find a certificate by email address. + * Is information about a valid encryption certificate for the given + * email address already available in the cache? + * + * @param emailAddress - The email address to check. + * + * @return - True if a valid cert is known by the cache. + */ + boolean haveValidCertForEmail(in AUTF8String emailAddress); + + /** + * If a valid encryption certificate for the given email address + * is already known by the cache, then return the NSS database + * key of that certificate. * - * (Note: This function is used primarily for testing purposes. It runs - * on the main thread, which may cause nested event loop spinning.) - * (TODO: A better place for this might be nsISMimeJSHelper.idl, - * but it would require to move the C++ implementation.) - * - * @param aEmailAddress - The email address to be used as the key - * - to find the certificate. - * @param aRequireValidCert - Set to False, to search for any certificate with a matching email address. - * Set to True, if the returned certificate must be currently valid - * and usable for email security. + * @param emailAddress - The email address to check. + * + * @return - NSS db key of the valid cert. + */ + ACString getCertDBKeyForEmail(in AUTF8String emailAddress); + + /** + * Remember the given certificate database key in our cache. The + * given certDBey (as used with nsIX509CertDB) must reference a + * valid encryption certificate for the given email address. + * + * @param emailAddress - The email address that is related to + * the given certDBKey. + * @param certDBKey - The certificate database key. + */ + void cacheValidCertForEmail(in AUTF8String emailAddress, + in ACString certDBKey); + + /* + * Asynchronously find an encryption certificate by email address. Calls + * `findCertDone` function on the provided `nsIDoneFindCertForEmailCallback` + * with the results of the operation. * - * @return The matching certificate if found. + * @param emailAddress - The email address to be used as the key + * to find the certificate. + * @param callback - A callback of type nsIDoneFindCertForEmailCallback, + * function findCertDone will be called with + * the result of the operation. */ [must_use] - nsIX509Cert findCertByEmailAddress(in ACString aEmailAddress, in boolean aRequireValidCert); + void asyncFindCertByEmailAddr(in AUTF8String emailAddress, + in nsIDoneFindCertForEmailCallback callback); }; diff -Nru thunderbird-102.9.0+build1/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.cpp thunderbird-102.10.0+build2/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.cpp --- thunderbird-102.9.0+build1/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.cpp 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mailnews/extensions/bayesian-spam-filter/nsBayesianFilter.cpp 2023-04-11 06:11:53.000000000 +0000 @@ -461,19 +461,19 @@ // extract the charset parameter nsCString parameterValue; - mimehdrpar->GetParameterInternal(headerValue.get(), "charset", - nullptr, nullptr, + mimehdrpar->GetParameterInternal(headerValue, "charset", nullptr, + nullptr, getter_Copies(parameterValue)); addTokenForHeader("charset", parameterValue); // create a token containing just the content type - mimehdrpar->GetParameterInternal(headerValue.get(), "type", nullptr, + mimehdrpar->GetParameterInternal(headerValue, "type", nullptr, nullptr, getter_Copies(parameterValue)); if (!parameterValue.Length()) mimehdrpar->GetParameterInternal( - headerValue.get(), nullptr /* use first unnamed param */, - nullptr, nullptr, getter_Copies(parameterValue)); + headerValue, nullptr /* use first unnamed param */, nullptr, + nullptr, getter_Copies(parameterValue)); addTokenForHeader("content-type/type", parameterValue); // XXX: should we add a token for the entire content-type header as diff -Nru thunderbird-102.9.0+build1/comm/mailnews/extensions/smime/moz.build thunderbird-102.10.0+build2/comm/mailnews/extensions/smime/moz.build --- thunderbird-102.9.0+build1/comm/mailnews/extensions/smime/moz.build 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mailnews/extensions/smime/moz.build 2023-04-11 06:11:53.000000000 +0000 @@ -13,7 +13,6 @@ "nsICMSSecureMessage.idl", "nsIEncryptedSMIMEURIsSrvc.idl", "nsIMsgSMIMEHeaderSink.idl", - "nsISMimeJSHelper.idl", "nsIUserCertPicker.idl", ] @@ -25,7 +24,6 @@ "nsCMSSecureMessage.cpp", "nsEncryptedSMIMEURIsService.cpp", "nsMsgComposeSecure.cpp", - "nsSMimeJSHelper.cpp", ] FINAL_LIBRARY = "mail" diff -Nru thunderbird-102.9.0+build1/comm/mailnews/extensions/smime/msgCompSecurityInfo.js thunderbird-102.10.0+build2/comm/mailnews/extensions/smime/msgCompSecurityInfo.js --- thunderbird-102.9.0+build1/comm/mailnews/extensions/smime/msgCompSecurityInfo.js 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mailnews/extensions/smime/msgCompSecurityInfo.js 2023-04-11 06:11:53.000000000 +0000 @@ -8,7 +8,7 @@ var gViewButton; var gBundle; -var gCerts; +var gCerts = []; window.addEventListener("DOMContentLoaded", onLoad); window.addEventListener("resize", resizeColumns); @@ -19,204 +19,47 @@ return; } - let helper = Cc[ - "@mozilla.org/messenger-smime/smimejshelper;1" - ].createInstance(Ci.nsISMimeJSHelper); - gListBox = document.getElementById("infolist"); gViewButton = document.getElementById("viewCertButton"); gBundle = document.getElementById("bundle_smime_comp_info"); - let allow_ldap_cert_fetching = - params.compFields.composeSecure.requireEncryptMessage; - - let emailAddresses = []; - let certIssuedInfos = []; - let certExpiresInfos = []; - let certs = []; - let canEncrypt = false; - - while (true) { - try { - // Out parameters - must be objects. - let outEmailAddresses = {}; - let outCertIssuedInfos = {}; - let outCertExpiresInfos = {}; - let outCerts = {}; - let outCanEncrypt = {}; - helper.getRecipientCertsInfo( - params.compFields, - outEmailAddresses, - outCertIssuedInfos, - outCertExpiresInfos, - outCerts, - outCanEncrypt - ); - // Unwrap to the actual values. - emailAddresses = outEmailAddresses.value; - certIssuedInfos = outCertIssuedInfos.value; - certExpiresInfos = outCertExpiresInfos.value; - gCerts = certs = outCerts.value; - canEncrypt = outCanEncrypt.value; - } catch (e) { - dump(e); - return; - } - - if (!allow_ldap_cert_fetching) { - break; - } - allow_ldap_cert_fetching = false; - - let missing = []; - for (let i = 0; i < emailAddresses.length; i++) { - if (!certs[i]) { - missing.push(emailAddresses[i]); - } - } + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); - if (missing.length > 0) { - var autocompleteLdap = Services.prefs.getBoolPref( - "ldap_2.autoComplete.useDirectory" - ); - - if (autocompleteLdap) { - var autocompleteDirectory = null; - if (params.currentIdentity.overrideGlobalPref) { - autocompleteDirectory = params.currentIdentity.directoryServer; - } else { - autocompleteDirectory = Services.prefs.getCharPref( - "ldap_2.autoComplete.directoryServer" - ); - } - - if (autocompleteDirectory) { - window.openDialog( - "chrome://messenger-smime/content/certFetchingStatus.xhtml", - "", - "chrome,resizable=1,modal=1,dialog=1", - autocompleteDirectory, - missing - ); - } - } - } - } + let missing = []; + for (let i = 0; i < params.recipients.length; i++) { + let email = params.recipients[i]; + let dbKey = params.compFields.composeSecure.getCertDBKeyForEmail(email); - let signedElement = document.getElementById("signed"); - let encryptedElement = document.getElementById("encrypted"); - if (params.compFields.composeSecure.requireEncryptMessage) { - if (params.isEncryptionCertAvailable && canEncrypt) { - encryptedElement.value = gBundle.getString("StatusYes"); + if (dbKey) { + gCerts.push(certdb.findCertByDBKey(dbKey)); } else { - encryptedElement.value = gBundle.getString("StatusNotPossible"); + gCerts.push(null); } - } else { - encryptedElement.value = gBundle.getString("StatusNo"); - } - if (params.compFields.composeSecure.signMessage) { - if (params.isSigningCertAvailable) { - signedElement.value = gBundle.getString("StatusYes"); - } else { - signedElement.value = gBundle.getString("StatusNotPossible"); + if (!gCerts[i]) { + missing.push(params.recipients[i]); } - } else { - signedElement.value = gBundle.getString("StatusNo"); } - for (let i = 0; i < emailAddresses.length; ++i) { + for (let i = 0; i < params.recipients.length; ++i) { let email = document.createXULElement("label"); - email.setAttribute("value", emailAddresses[i]); + email.setAttribute("value", params.recipients[i]); email.setAttribute("crop", "end"); email.setAttribute("style", "width: var(--recipientWidth)"); let listitem = document.createXULElement("richlistitem"); listitem.appendChild(email); - if (!certs[i]) { - let notFound = document.createXULElement("label"); - notFound.setAttribute("value", gBundle.getString("StatusNotFound")); - notFound.setAttribute("style", "width: var(--statusWidth)"); - listitem.appendChild(notFound); - } else { - let status = document.createXULElement("label"); - status.setAttribute("value", "?"); // temporary placeholder - status.setAttribute("crop", "end"); - status.setAttribute("style", "width: var(--statusWidth)"); - listitem.appendChild(status); - - let issued = document.createXULElement("label"); - issued.setAttribute("value", certIssuedInfos[i]); - issued.setAttribute("crop", "end"); - issued.setAttribute("style", "width: var(--issuedWidth)"); - listitem.appendChild(issued); - - let expire = document.createXULElement("label"); - expire.setAttribute("value", certExpiresInfos[i]); - expire.setAttribute("crop", "end"); - expire.setAttribute("style", "width: var(--expireWidth)"); - listitem.appendChild(expire); - - asyncDetermineUsages(certs[i]).then(results => { - let someError = results.some( - result => result.errorCode !== PRErrorCodeSuccess - ); - if (!someError) { - status.setAttribute("value", gBundle.getString("StatusValid")); - return; - } - - // Keep in sync with certViewer.js. - const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; - const SEC_ERROR_EXPIRED_CERTIFICATE = SEC_ERROR_BASE + 11; - const SEC_ERROR_REVOKED_CERTIFICATE = SEC_ERROR_BASE + 12; - const SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13; - const SEC_ERROR_UNTRUSTED_ISSUER = SEC_ERROR_BASE + 20; - const SEC_ERROR_UNTRUSTED_CERT = SEC_ERROR_BASE + 21; - const SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE = SEC_ERROR_BASE + 30; - const SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED = - SEC_ERROR_BASE + 176; - - const errorRankings = [ - { - error: SEC_ERROR_REVOKED_CERTIFICATE, - bundleString: "StatusRevoked", - }, - { error: SEC_ERROR_UNTRUSTED_CERT, bundleString: "StatusUntrusted" }, - { - error: SEC_ERROR_UNTRUSTED_ISSUER, - bundleString: "StatusUntrusted", - }, - { - error: SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED, - bundleString: "StatusInvalid", - }, - { - error: SEC_ERROR_EXPIRED_CERTIFICATE, - bundleString: "StatusExpired", - }, - { - error: SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE, - bundleString: "StatusExpired", - }, - { error: SEC_ERROR_UNKNOWN_ISSUER, bundleString: "StatusUntrusted" }, - ]; - - let bs = "StatusInvalid"; - for (let errorRanking of errorRankings) { - let errorPresent = results.some( - result => result.errorCode == errorRanking.error - ); - if (errorPresent) { - bs = errorRanking.bundleString; - break; - } - } - - status.setAttribute("value", gBundle.getString(bs)); - }); - } + let cert = gCerts[i]; + let statusItem = document.createXULElement("label"); + statusItem.setAttribute( + "value", + gBundle.getString(cert ? "StatusValid" : "StatusNotFound") + ); + statusItem.setAttribute("style", "width: var(--statusWidth)"); + listitem.appendChild(statusItem); gListBox.appendChild(listitem); } @@ -255,37 +98,6 @@ certificateUsageEmailRecipient, }; -function asyncDetermineUsages(cert) { - let promises = []; - let now = Date.now() / 1000; - let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( - Ci.nsIX509CertDB - ); - Object.keys(certificateUsages).forEach(usageString => { - promises.push( - new Promise((resolve, reject) => { - let usage = certificateUsages[usageString]; - certdb.asyncVerifyCertAtTime( - cert, - usage, - 0, - null, - now, - (aPRErrorCode, aVerifiedChain, aHasEVPolicy) => { - resolve({ - usageString, - errorCode: aPRErrorCode, - chain: aVerifiedChain, - }); - } - ); - }) - ); - }); - return Promise.all(promises); -} -// --- /borrowed from pippki.js --- - function onSelectionChange(event) { gViewButton.disabled = !( gListBox.selectedItems.length == 1 && certForRow(gListBox.selectedIndex) diff -Nru thunderbird-102.9.0+build1/comm/mailnews/extensions/smime/msgCompSecurityInfo.xhtml thunderbird-102.10.0+build2/comm/mailnews/extensions/smime/msgCompSecurityInfo.xhtml --- thunderbird-102.9.0+build1/comm/mailnews/extensions/smime/msgCompSecurityInfo.xhtml 2023-03-11 11:25:08.000000000 +0000 +++ thunderbird-102.10.0+build2/comm/mailnews/extensions/smime/msgCompSecurityInfo.xhtml 2023-04-11 06:11:53.000000000 +0000 @@ -22,20 +22,6 @@ - &subject.plaintextWarning; - - &status.heading; - - - - - - -