From c4ee32e9ee320de2379c4f4e9493d5a976cde248 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:31:29 -0700 Subject: [PATCH] Use protopiler for protocol buffers Co-authored-by: Jamie Kyle --- .eslint/rules/file-suffix.js | 2 - ACKNOWLEDGMENTS.md | 228 +- .../protopiler-migration/01-remove-finish.js | 35 + codemods/protopiler-migration/02-remove-i.js | 34 + codemods/protopiler-migration/03-to-number.js | 76 + .../04-long-from-number.js | 53 + package.json | 25 +- patches/protobufjs+7.3.2.patch | 962 ------- patches/protobufjs-cli+1.1.1.patch | 18 - pnpm-lock.yaml | 293 +- protos/ArtCreator.proto | 14 - protos/ContactDiscovery.proto | 53 - protos/SignalService.proto | 89 +- protos/SubProtocol.proto | 34 - ts/Crypto.node.ts | 3 +- ts/SignalProtocolStore.preload.ts | 1 + ts/groups.preload.ts | 1293 ++++----- ts/groups/joinViaLink.preload.ts | 2 +- ts/groups/util.std.ts | 11 +- ts/jobs/helpers/sendCallingMessage.preload.ts | 6 +- .../helpers/sendDeleteForEveryone.preload.ts | 6 +- .../sendDeleteStoryForEveryone.preload.ts | 14 +- ...sendDirectExpirationTimerUpdate.preload.ts | 6 +- ts/jobs/helpers/sendNormalMessage.preload.ts | 2 +- ts/jobs/helpers/sendPollTerminate.preload.ts | 2 +- ts/jobs/helpers/sendStory.preload.ts | 33 +- ts/models/conversations.preload.ts | 22 +- ts/protobuf/index.std.ts | 2 - ts/protobuf/wrap.std.ts | 10 - ts/services/backups/constants.std.ts | 2 +- ts/services/backups/errors.std.ts | 4 +- ts/services/backups/export.preload.ts | 2374 +++++++++-------- ts/services/backups/import.preload.ts | 894 ++++--- .../backups/util/filePointers.preload.ts | 138 +- ts/services/backups/util/localBackup.node.ts | 21 +- ts/services/calling.preload.ts | 59 +- ts/services/storage.preload.ts | 292 +- ts/services/storageRecordOps.preload.ts | 1110 ++++---- ts/sql/cleanDataForIpc.std.ts | 9 +- .../migrations/1280-blob-unprocessed.std.ts | 5 +- ts/sql/migrations/89-call-history.node.ts | 4 +- ts/state/ducks/calling.preload.ts | 31 +- .../ContactsParser_test.preload.ts | 23 +- .../MessageReceiver_test.preload.ts | 17 +- .../SignalProtocolStore_test.preload.ts | 94 +- .../backup/filePointer_test.preload.ts | 195 +- .../backup/non_bubble_test.preload.ts | 29 +- ts/test-helpers/generateBackup.node.ts | 162 +- .../pnp/accept_gv2_invite_test.node.ts | 2 +- ts/test-node/Proto_unknown_field_test.std.ts | 141 - .../groups/add_banned_member_test.preload.ts | 8 +- .../processDataMessage_test.preload.ts | 109 +- ts/test-node/processSent_test.node.ts | 58 + ts/test-node/processSyncMessage_test.node.ts | 38 - ts/test-node/sql/cleanDataForIpc_test.std.ts | 21 - ts/test-node/sql/migration_1280_test.node.ts | 25 +- .../arePinnedConversationsEqual_test.node.ts | 86 +- .../util/callingMessageToProto_test.node.ts | 6 +- .../util/sessionTranslation_test.node.ts | 45 +- .../util/timestampLongUtils_test.std.ts | 62 +- ts/textsecure/AccountManager.preload.ts | 11 +- ts/textsecure/ContactsParser.preload.ts | 17 +- ts/textsecure/MessageReceiver.preload.ts | 354 ++- ts/textsecure/OutgoingMessage.preload.ts | 60 +- ts/textsecure/SendMessage.preload.ts | 1790 +++++++------ ts/textsecure/Types.d.ts | 34 +- ts/textsecure/WebAPI.preload.ts | 22 +- ts/textsecure/messageReceiverEvents.std.ts | 11 +- ts/textsecure/processDataMessage.preload.ts | 197 +- ts/textsecure/processSyncMessage.node.ts | 48 +- ts/types/Attachment.std.ts | 9 +- ts/types/CallDisposition.std.ts | 15 +- ts/types/LinkPreview.std.ts | 2 +- ts/types/Stickers.preload.ts | 10 +- ts/util/BodyRange.node.ts | 5 +- ts/util/ServiceId.node.ts | 6 + ts/util/arePinnedConversationsEqual.node.ts | 13 +- ts/util/attachments.preload.ts | 8 +- ts/util/backupSubscriptionData.preload.ts | 52 +- ts/util/callDisposition.preload.ts | 93 +- ts/util/callingMessageToProto.node.ts | 36 +- ts/util/checkFirstEnvelope.dom.ts | 4 +- ts/util/computeBlurHashUrl.std.ts | 3 +- ts/util/deleteStoryForEveryone.preload.ts | 7 +- ts/util/denyPendingApprovalRequest.preload.ts | 2 +- ts/util/encodeDelimited.std.ts | 36 + ts/util/handleRetry.preload.ts | 17 +- ts/util/inspectProtobufs.std.ts | 4 +- ts/util/isKnownProtoEnumMember.std.ts | 9 + ts/util/long.std.ts | 4 + ts/util/onStoryRecipientUpdate.preload.ts | 12 +- ts/util/removePendingMember.preload.ts | 2 +- ts/util/sendCallLinkUpdateSync.preload.ts | 22 +- ts/util/sendToGroup.preload.ts | 18 +- ts/util/sessionTranslation.node.ts | 177 +- ts/util/timestampLongUtils.std.ts | 29 +- ts/util/uploadAttachment.preload.ts | 27 +- 97 files changed, 6197 insertions(+), 6362 deletions(-) create mode 100644 codemods/protopiler-migration/01-remove-finish.js create mode 100644 codemods/protopiler-migration/02-remove-i.js create mode 100644 codemods/protopiler-migration/03-to-number.js create mode 100644 codemods/protopiler-migration/04-long-from-number.js delete mode 100644 patches/protobufjs+7.3.2.patch delete mode 100644 patches/protobufjs-cli+1.1.1.patch delete mode 100644 protos/ArtCreator.proto delete mode 100644 protos/ContactDiscovery.proto delete mode 100644 protos/SubProtocol.proto delete mode 100644 ts/protobuf/wrap.std.ts delete mode 100644 ts/test-node/Proto_unknown_field_test.std.ts create mode 100644 ts/test-node/processSent_test.node.ts delete mode 100644 ts/test-node/processSyncMessage_test.node.ts create mode 100644 ts/util/encodeDelimited.std.ts create mode 100644 ts/util/isKnownProtoEnumMember.std.ts create mode 100644 ts/util/long.std.ts diff --git a/.eslint/rules/file-suffix.js b/.eslint/rules/file-suffix.js index a6cafa966b..912dff3eda 100644 --- a/.eslint/rules/file-suffix.js +++ b/.eslint/rules/file-suffix.js @@ -114,7 +114,6 @@ const NODE_PACKAGES = new Set([ 'postcss-loader', 'prettier', 'prettier-plugin-tailwindcss', - 'protobufjs-cli', 'react-devtools', 'react-devtools-core', 'resedit', @@ -247,7 +246,6 @@ const STD_PACKAGES = new Set([ 'js-yaml', 'linkify-it', 'lodash', - 'long', 'lru-cache', 'memoizee', 'mocha', diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index f4dbec4a70..1b9ce13b6d 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -108,6 +108,28 @@ Signal Desktop makes use of the following open source projects. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## @indutny/protopiler + + Copyright Fedor Indutny, 2026. + + 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. + ## @indutny/range-finder Copyright Fedor Indutny, 2022. @@ -4528,210 +4550,6 @@ Signal Desktop makes use of the following open source projects. licenses; we recommend you read them, as their terms may differ from the terms above. -## long - - 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. - ## lru-cache The ISC License @@ -14604,7 +14422,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see ``` -## libsignal-account-keys 0.1.0, libsignal-core 0.1.0, mrp 2.66.0, protobuf 2.66.0, ringrtc 2.66.0, regex-aot 0.1.0, partial-default-derive 0.1.0, partial-default 0.1.0 +## libsignal-account-keys 0.1.0, libsignal-core 0.1.0, mrp 2.66.1, protobuf 2.66.1, ringrtc 2.66.1, regex-aot 0.1.0, partial-default-derive 0.1.0, partial-default 0.1.0 ``` GNU AFFERO GENERAL PUBLIC LICENSE diff --git a/codemods/protopiler-migration/01-remove-finish.js b/codemods/protopiler-migration/01-remove-finish.js new file mode 100644 index 0000000000..8bf887330c --- /dev/null +++ b/codemods/protopiler-migration/01-remove-finish.js @@ -0,0 +1,35 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +export default function transform() { + return { + visitor: { + CallExpression(path) { + const { node } = path; + if (node.arguments.length !== 0) { + return; + } + if (node.callee.type !== 'MemberExpression') { + return; + } + const { object, property } = node.callee; + if (object.type !== 'CallExpression') { + return; + } + + if (property.type !== 'Identifier' || property.name !== 'finish') { + return; + } + + if ( + object.callee.type !== 'MemberExpression' || + object.callee.property.type !== 'Identifier' || + object.callee.property.name !== 'encode' + ) { + return; + } + + path.replaceWith(object); + }, + }, + }; +} diff --git a/codemods/protopiler-migration/02-remove-i.js b/codemods/protopiler-migration/02-remove-i.js new file mode 100644 index 0000000000..63fcea00c8 --- /dev/null +++ b/codemods/protopiler-migration/02-remove-i.js @@ -0,0 +1,34 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +export default function transform(babel) { + const { types: t } = babel; + + return { + visitor: { + TSQualifiedName(path) { + const { node } = path; + if ( + node.right.type !== 'Identifier' || + !/^I[A-Z][a-z]/.test(node.right.name) + ) { + return; + } + + // Don't touch fuse.js + if (node.left.type === 'Identifier' && node.left.name === 'Fuse') { + return; + } + + path.replaceWith( + t.TSQualifiedName( + t.TSQualifiedName( + node.left, + t.Identifier(node.right.name.slice(1)) + ), + t.Identifier('Params') + ) + ); + }, + }, + }; +} diff --git a/codemods/protopiler-migration/03-to-number.js b/codemods/protopiler-migration/03-to-number.js new file mode 100644 index 0000000000..493c092e91 --- /dev/null +++ b/codemods/protopiler-migration/03-to-number.js @@ -0,0 +1,76 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { relative, dirname, join } from 'node:path'; + +const importPath = join(__dirname, '..', 'ts', 'util', 'toNumber.std.js'); + +export default function transform(babel) { + const { types: t } = babel; + + let program; + function Program({ node }) { + program = node; + } + + function addImport(filename) { + if (program === undefined) { + return; + } + + let index = program.body.findLastIndex( + stmt => stmt.type === 'ImportDeclaration' + ); + if (index === -1) { + index = 0; + } else { + index += 1; + } + let relativePath = relative(dirname(filename), importPath); + if (!relativePath.startsWith('.')) { + relativePath = `./${relativePath}`; + } + program.body.splice( + index, + 0, + t.ImportDeclaration( + [t.ImportSpecifier(t.Identifier('toNumber'), t.Identifier('toNumber'))], + t.StringLiteral(relativePath) + ) + ); + program = undefined; + } + + function CallExpression(path, { filename }) { + const { callee, arguments: args } = path.node; + if (args.length !== 0) { + return; + } + if ( + callee.type !== 'MemberExpression' && + callee.type !== 'OptionalMemberExpression' + ) { + return; + } + if ( + callee.property.type !== 'Identifier' || + callee.property.name !== 'toNumber' + ) { + return; + } + + const replacement = t.CallExpression(t.Identifier('toNumber'), [ + callee.object, + ]); + path.replaceWith(replacement); + addImport(filename); + } + + return { + visitor: { + Program, + CallExpression, + OptionalCallExpression: CallExpression, + }, + }; +} diff --git a/codemods/protopiler-migration/04-long-from-number.js b/codemods/protopiler-migration/04-long-from-number.js new file mode 100644 index 0000000000..9dc57b411a --- /dev/null +++ b/codemods/protopiler-migration/04-long-from-number.js @@ -0,0 +1,53 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +export default function transform(babel) { + const { types: t } = babel; + + return { + visitor: { + CallExpression(path) { + const { node } = path; + if (node.arguments.length !== 1) { + return; + } + if (node.callee.type !== 'MemberExpression') { + return; + } + const { object, property } = node.callee; + if (object.type !== 'Identifier' || object.name !== 'Long') { + return; + } + if (property.type !== 'Identifier') { + return; + } + + if (property.name === 'isLong') { + path.replaceWith( + t.BinaryExpression( + '===', + t.UnaryExpression('typeof', node.arguments[0]), + t.StringLiteral('bigint') + ) + ); + return; + } + + if ( + property.name !== 'fromNumber' && + property.name !== 'fromString' && + property.name !== 'fromValue' + ) { + return; + } + + if (node.arguments[0].type === 'NumericLiteral') { + path.replaceWith(t.BigIntLiteral(node.arguments[0].value.toString())); + return; + } + path.replaceWith( + t.CallExpression(t.Identifier('BigInt'), [node.arguments[0]]) + ); + }, + }, + }; +} diff --git a/package.json b/package.json index fe9a004120..38851a9375 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "generate": "run-s generate:phase-0 generate:phase-1", "generate:phase-0": "run-p build:esbuild:scripts", "generate:phase-1": "run-p --aggregate-output --print-label generate:phase-1:bundle build:icu-types build:compact-locales build:styles get-expire-time copy-components build:policy-files", - "generate:phase-1:bundle": "run-s build-protobuf build:esbuild:bundle", + "generate:phase-1:bundle": "run-s build:protobuf build:esbuild:bundle", "build-release": "pnpm run build", "sign-release": "node ts/updater/generateSignature.js", "notarize": "echo 'No longer necessary'", @@ -33,10 +33,8 @@ "mark-unusued-strings-deleted": "ts-node ./ts/scripts/mark-unused-strings-deleted.node.ts", "get-expire-time": "node ts/scripts/get-expire-time.node.js", "copy-components": "node ts/scripts/copy.node.js", - "build-module-protobuf": "pbjs --root='signal-desktop' --target static-module --force-long --no-typeurl --no-verify --no-create --no-convert --wrap commonjs --out ts/protobuf/compiled.std.js protos/*.proto && pbts --no-comments --out ts/protobuf/compiled.std.d.ts ts/protobuf/compiled.std.js", - "clean-module-protobuf": "rm -f ts/protobuf/compiled.std.d.ts ts/protobuf/compiled.std.js", - "build-protobuf": "pnpm run build-module-protobuf", - "clean-protobuf": "pnpm run clean-module-protobuf", + "build:protobuf": "protopiler --module cjs --output ts/protobuf/compiled.std.js --typedefs ts/protobuf/compiled.std.d.ts protos", + "clean:protobuf": "rm -f ts/protobuf/compiled.std.d.ts ts/protobuf/compiled.std.js", "prepare-beta-build": "node scripts/prepare_beta_build.js", "prepare-alpha-build": "node scripts/prepare_alpha_build.js", "prepare-alpha-version": "node scripts/prepare_tagged_version.js alpha", @@ -69,15 +67,15 @@ "check:types": "tsc --noEmit", "clean-transpile": "node ./scripts/clean-transpile.js", "ready": "npm-run-all --print-label clean-transpile generate --parallel lint lint-deps lint-intl test-node test-electron", - "dev": "pnpm run build-protobuf && cross-env SIGNAL_ENV=storybook storybook dev --port 6006", + "dev": "pnpm run build:protobuf && cross-env SIGNAL_ENV=storybook storybook dev --port 6006", "dev:transpile": "run-p \"check:types --watch\" dev:esbuild dev:icu-types dev:protobuf", "dev:esbuild": "node scripts/esbuild.js --watch", "dev:styles": "pnpm run '/^dev:styles:(sass|tailwind)$/'", "dev:styles:sass": "pnpm run build:styles:sass --watch", "dev:styles:tailwind": "pnpm run build:styles:tailwind --watch", "dev:icu-types": "chokidar ./_locales/en/messages.json --initial --command \"pnpm run build:icu-types\"", - "dev:protobuf": "chokidar ./protos/**/*.proto --command \"pnpm run build-protobuf\"", - "build:storybook": "pnpm run build-protobuf && cross-env SIGNAL_ENV=storybook storybook build", + "dev:protobuf": "chokidar ./protos/**/*.proto --command \"pnpm run build:protobuf\"", + "build:storybook": "pnpm run build:protobuf && cross-env SIGNAL_ENV=storybook storybook build", "test:storybook": "pnpm run build:storybook && run-p --race test:storybook:*", "test:storybook:serve": "http-server storybook-static --port 6006 --silent", "test:storybook:test": "wait-on http://127.0.0.1:6006/ --timeout 5000 && test-storybook --testTimeout 60000", @@ -123,6 +121,7 @@ "@formatjs/intl-localematcher": "0.2.32", "@indutny/dicer": "0.3.2", "@indutny/mac-screen-share": "1.0.13", + "@indutny/protopiler": "3.1.1", "@indutny/range-finder": "1.3.4", "@indutny/simple-windows-notifications": "2.0.16", "@indutny/sneequals": "4.0.0", @@ -138,7 +137,7 @@ "@signalapp/minimask": "1.0.1", "@signalapp/mute-state-change": "workspace:1.0.0", "@signalapp/quill-cjs": "2.1.2", - "@signalapp/ringrtc": "2.66.0", + "@signalapp/ringrtc": "2.66.1", "@signalapp/sqlcipher": "2.4.4", "@signalapp/windows-ucv": "1.0.1", "@tanstack/react-virtual": "3.11.2", @@ -181,7 +180,6 @@ "js-yaml": "4.1.0", "linkify-it": "5.0.0", "lodash": "4.17.21", - "long": "5.2.3", "lru-cache": "11.0.2", "memoizee": "0.4.17", "moment": "2.30.1", @@ -240,7 +238,7 @@ "@indutny/parallel-prettier": "3.0.0", "@indutny/rezip-electron": "3.0.2", "@napi-rs/canvas": "0.1.61", - "@signalapp/mock-server": "18.0.0", + "@signalapp/mock-server": "18.1.0", "@storybook/addon-a11y": "8.4.4", "@storybook/addon-actions": "8.4.4", "@storybook/addon-controls": "8.4.4", @@ -345,7 +343,6 @@ "postcss-loader": "8.1.1", "prettier": "3.7.4", "prettier-plugin-tailwindcss": "0.7.2", - "protobufjs-cli": "1.1.1", "react-devtools": "6.0.1", "react-devtools-core": "6.0.1", "resedit": "2.0.2", @@ -372,7 +369,7 @@ }, "pnpm": { "overrides": { - "@storybook/core>node-fetch": "$node-fetch", + "@storybook/core>node-fetch": "2.6.7", "eslint-config-airbnb-typescript-prettier>eslint-plugin-prettier": "5.2.1", "canvas": "-", "jsdom": "-", @@ -383,9 +380,7 @@ }, "patchedDependencies": { "casual@1.6.2": "patches/casual+1.6.2.patch", - "protobufjs@7.3.2": "patches/protobufjs+7.3.2.patch", "@types/express@4.17.21": "patches/@types+express+4.17.21.patch", - "protobufjs-cli@1.1.1": "patches/protobufjs-cli+1.1.1.patch", "@types/fabric@4.5.3": "patches/@types+fabric+4.5.3.patch", "qrcode-generator@1.4.4": "patches/qrcode-generator+1.4.4.patch", "@types/node-fetch@2.6.12": "patches/@types+node-fetch+2.6.12.patch", diff --git a/patches/protobufjs+7.3.2.patch b/patches/protobufjs+7.3.2.patch deleted file mode 100644 index 3001c3c96c..0000000000 --- a/patches/protobufjs+7.3.2.patch +++ /dev/null @@ -1,962 +0,0 @@ -diff --git a/dist/light/protobuf.js b/dist/light/protobuf.js -index 4862f59..989dc47 100644 ---- a/dist/light/protobuf.js -+++ b/dist/light/protobuf.js -@@ -1,6 +1,6 @@ - /*! - * protobuf.js v7.3.2 (c) 2016, daniel wirtz -- * compiled wed, 12 jun 2024 08:24:21 utc -+ * compiled mon, 24 jun 2024 23:18:03 utc - * licensed under the bsd-3-clause license - * see: https://github.com/dcodeio/protobuf.js for details - */ -@@ -1435,6 +1435,7 @@ function decoder(mtype) { - ("r=Reader.create(r)") - ("var c=l===undefined?r.len:r.pos+l,m=new this.ctor" + (mtype.fieldsArray.filter(function(field) { return field.map; }).length ? ",k,value" : "")) - ("while(r.pos>>3){"); - -+ var unknownRef = "m" + util.safeProp("$unknownFields"); -+ - var i = 0; - for (; i < /* initializes */ mtype.fieldsArray.length; ++i) { - var field = mtype._fieldsArray[i].resolve(), -@@ -1524,6 +1527,11 @@ function decoder(mtype) { - } gen - ("default:") - ("r.skipType(t&7)") -+ ("if (!(%s)) {", unknownRef) -+ ("%s = []", unknownRef) -+ ("}") -+ -+ ("%s.push(r.buf.slice(unknownStartPos, r.pos))", unknownRef) - ("break") - - ("}") -@@ -1581,6 +1589,21 @@ function encoder(mtype) { - // "when a message is serialized its known fields should be written sequentially by field number" - var fields = /* initializes */ mtype.fieldsArray.slice().sort(util.compareFieldsById); - -+ var unknownRef = "m" + util.safeProp("$unknownFields"); -+ -+ // Redecode unknown fields and apply them to the message before encoding -+ gen -+ ("var fullyUnknown=[]") -+ ("if(%s&&this.ctor.decode) {", unknownRef) -+ ("for(var i=0;i<%s.length;++i) {", unknownRef) -+ ("try {") -+ ("var known=this.ctor.decode(%s[i])", unknownRef) -+ ("fullyUnknown=fullyUnknown.concat(known.$unknownFields||[])") -+ ("m=Object.assign(known,m)") -+ ("}catch(_){}") -+ ("}") -+ ("}"); -+ - for (var i = 0; i < fields.length; ++i) { - var field = fields[i].resolve(), - index = mtype._fieldsArray.indexOf(field), -@@ -1639,6 +1662,11 @@ function encoder(mtype) { - } - } - -+ gen -+ ("for(var i=0;i>2],r=(3&h)<<4,o=1;break;case 1:s[u++]=f[r|h>>4],r=(15&h)<<2,o=2;break;case 2:s[u++]=f[r|h>>6],s[u++]=f[63&h],o=0}8191>4,r=o,s=2;break;case 2:i[n++]=(15&r)<<4|(60&o)>>2,r=o,s=3;break;case 3:i[n++]=(3&r)<<6|o,s=0}}if(1===s)throw Error(c);return n-e},n.test=function(t){return/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(t)}},{}],3:[function(t,i,n){function a(i,n){"string"==typeof i&&(n=i,i=g);var h=[];function f(t){if("string"!=typeof t){var i=c();if(a.verbose&&console.log("codegen: "+i),i="return "+i,t){for(var n=Object.keys(t),r=Array(n.length+1),e=Array(n.length),s=0;s>>0:i<11754943508222875e-54?(e<<31|Math.round(i/1401298464324817e-60))>>>0:(e<<31|127+(t=Math.floor(Math.log(i)/Math.LN2))<<23|8388607&Math.round(i*Math.pow(2,-t)*8388608))>>>0,n,r)}function n(t,i,n){t=t(i,n),i=2*(t>>31)+1,n=t>>>23&255,t&=8388607;return 255==n?t?NaN:1/0*i:0==n?1401298464324817e-60*i*t:i*Math.pow(2,n-150)*(8388608+t)}function r(t,i,n){o[0]=t,i[n]=h[0],i[n+1]=h[1],i[n+2]=h[2],i[n+3]=h[3]}function e(t,i,n){o[0]=t,i[n]=h[3],i[n+1]=h[2],i[n+2]=h[1],i[n+3]=h[0]}function s(t,i){return h[0]=t[i],h[1]=t[i+1],h[2]=t[i+2],h[3]=t[i+3],o[0]}function u(t,i){return h[3]=t[i],h[2]=t[i+1],h[1]=t[i+2],h[0]=t[i+3],o[0]}var o,h,f,c,a;function l(t,i,n,r,e,s){var u,o=r<0?1:0;0===(r=o?-r:r)?(t(0,e,s+i),t(0<1/r?0:2147483648,e,s+n)):isNaN(r)?(t(0,e,s+i),t(2146959360,e,s+n)):17976931348623157e292>>0,e,s+n)):r<22250738585072014e-324?(t((u=r/5e-324)>>>0,e,s+i),t((o<<31|u/4294967296)>>>0,e,s+n)):(t(4503599627370496*(u=r*Math.pow(2,-(r=1024===(r=Math.floor(Math.log(r)/Math.LN2))?1023:r)))>>>0,e,s+i),t((o<<31|r+1023<<20|1048576*u&1048575)>>>0,e,s+n))}function d(t,i,n,r,e){i=t(r,e+i),t=t(r,e+n),r=2*(t>>31)+1,e=t>>>20&2047,n=4294967296*(1048575&t)+i;return 2047==e?n?NaN:1/0*r:0==e?5e-324*r*n:r*Math.pow(2,e-1075)*(n+4503599627370496)}function v(t,i,n){f[0]=t,i[n]=c[0],i[n+1]=c[1],i[n+2]=c[2],i[n+3]=c[3],i[n+4]=c[4],i[n+5]=c[5],i[n+6]=c[6],i[n+7]=c[7]}function b(t,i,n){f[0]=t,i[n]=c[7],i[n+1]=c[6],i[n+2]=c[5],i[n+3]=c[4],i[n+4]=c[3],i[n+5]=c[2],i[n+6]=c[1],i[n+7]=c[0]}function p(t,i){return c[0]=t[i],c[1]=t[i+1],c[2]=t[i+2],c[3]=t[i+3],c[4]=t[i+4],c[5]=t[i+5],c[6]=t[i+6],c[7]=t[i+7],f[0]}function y(t,i){return c[7]=t[i],c[6]=t[i+1],c[5]=t[i+2],c[4]=t[i+3],c[3]=t[i+4],c[2]=t[i+5],c[1]=t[i+6],c[0]=t[i+7],f[0]}return"undefined"!=typeof Float32Array?(o=new Float32Array([-0]),h=new Uint8Array(o.buffer),a=128===h[3],t.writeFloatLE=a?r:e,t.writeFloatBE=a?e:r,t.readFloatLE=a?s:u,t.readFloatBE=a?u:s):(t.writeFloatLE=i.bind(null,m),t.writeFloatBE=i.bind(null,w),t.readFloatLE=n.bind(null,g),t.readFloatBE=n.bind(null,j)),"undefined"!=typeof Float64Array?(f=new Float64Array([-0]),c=new Uint8Array(f.buffer),a=128===c[7],t.writeDoubleLE=a?v:b,t.writeDoubleBE=a?b:v,t.readDoubleLE=a?p:y,t.readDoubleBE=a?y:p):(t.writeDoubleLE=l.bind(null,m,0,4),t.writeDoubleBE=l.bind(null,w,4,0),t.readDoubleLE=d.bind(null,g,0,4),t.readDoubleBE=d.bind(null,j,4,0)),t}function m(t,i,n){i[n]=255&t,i[n+1]=t>>>8&255,i[n+2]=t>>>16&255,i[n+3]=t>>>24}function w(t,i,n){i[n]=t>>>24,i[n+1]=t>>>16&255,i[n+2]=t>>>8&255,i[n+3]=255&t}function g(t,i){return(t[i]|t[i+1]<<8|t[i+2]<<16|t[i+3]<<24)>>>0}function j(t,i){return(t[i]<<24|t[i+1]<<16|t[i+2]<<8|t[i+3])>>>0}i.exports=r(r)},{}],7:[function(t,i,n){function r(t){try{var i=eval("require")(t);if(i&&(i.length||Object.keys(i).length))return i}catch(t){}return null}i.exports=r},{}],8:[function(t,i,n){var e=n.isAbsolute=function(t){return/^(?:\/|\w+:)/.test(t)},r=n.normalize=function(t){var i=(t=t.replace(/\\/g,"/").replace(/\/{2,}/g,"/")).split("/"),n=e(t),t="";n&&(t=i.shift()+"/");for(var r=0;r>>1,s=null,u=r;return function(t){if(t<1||e>10),s[u++]=56320+(1023&r)):s[u++]=(15&r)<<12|(63&t[i++])<<6|63&t[i++],8191>6|192:(55296==(64512&r)&&56320==(64512&(e=t.charCodeAt(u+1)))?(++u,i[n++]=(r=65536+((1023&r)<<10)+(1023&e))>>18|240,i[n++]=r>>12&63|128):i[n++]=r>>12|224,i[n++]=r>>6&63|128),i[n++]=63&r|128);return n-s}},{}],11:[function(t,i,n){var l=t(14),d=t(33);function u(t,i,n,r){var e=!1;if(i.resolvedType)if(i.resolvedType instanceof l){t("switch(d%s){",r);for(var s=i.resolvedType.values,u=Object.keys(s),o=0;o>>0",r,r);break;case"int32":case"sint32":case"sfixed32":t("m%s=d%s|0",r,r);break;case"uint64":h=!0;case"int64":case"sint64":case"fixed64":case"sfixed64":t("if(util.Long)")("(m%s=util.Long.fromValue(d%s)).unsigned=%j",r,r,h)('else if(typeof d%s==="string")',r)("m%s=parseInt(d%s,10)",r,r)('else if(typeof d%s==="number")',r)("m%s=d%s",r,r)('else if(typeof d%s==="object")',r)("m%s=new util.LongBits(d%s.low>>>0,d%s.high>>>0).toNumber(%s)",r,r,r,h?"true":"");break;case"bytes":t('if(typeof d%s==="string")',r)("util.base64.decode(d%s,m%s=util.newBuffer(util.base64.length(d%s)),0)",r,r,r)("else if(d%s.length >= 0)",r)("m%s=d%s",r,r);break;case"string":t("m%s=String(d%s)",r,r);break;case"bool":t("m%s=Boolean(d%s)",r,r)}}return t}function v(t,i,n,r){if(i.resolvedType)i.resolvedType instanceof l?t("d%s=o.enums===String?(types[%i].values[m%s]===undefined?m%s:types[%i].values[m%s]):m%s",r,n,r,r,n,r,r):t("d%s=types[%i].toObject(m%s,o)",r,n,r);else{var e=!1;switch(i.type){case"double":case"float":t("d%s=o.json&&!isFinite(m%s)?String(m%s):m%s",r,r,r,r);break;case"uint64":e=!0;case"int64":case"sint64":case"fixed64":case"sfixed64":t('if(typeof m%s==="number")',r)("d%s=o.longs===String?String(m%s):m%s",r,r,r)("else")("d%s=o.longs===String?util.Long.prototype.toString.call(m%s):o.longs===Number?new util.LongBits(m%s.low>>>0,m%s.high>>>0).toNumber(%s):m%s",r,r,r,r,e?"true":"",r);break;case"bytes":t("d%s=o.bytes===String?util.base64.encode(m%s,0,m%s.length):o.bytes===Array?Array.prototype.slice.call(m%s):m%s",r,r,r,r,r);break;default:t("d%s=m%s",r,r)}}return t}n.fromObject=function(t){var i=t.fieldsArray,n=d.codegen(["d"],t.name+"$fromObject")("if(d instanceof this.ctor)")("return d");if(!i.length)return n("return new this.ctor");n("var m=new this.ctor");for(var r=0;r>>3){");for(var n=0;n>>3){")("case 1: k=r.%s(); break",r.keyType)("case 2:"),h.basic[e]===g?i("value=types[%i].decode(r,r.uint32())",n):i("value=r.%s()",e),i("break")("default:")("r.skipType(tag2&7)")("break")("}")("}"),h.long[r.keyType]!==g?i('%s[typeof k==="object"?util.longToHash(k):k]=value',s):i("%s[k]=value",s)):r.repeated?(i("if(!(%s&&%s.length))",s,s)("%s=[]",s),h.packed[e]!==g&&i("if((t&7)===2){")("var c2=r.uint32()+r.pos")("while(r.pos>>0,8|c.mapKey[s.keyType],s.keyType),h===g?n("types[%i].encode(%s[ks[i]],w.uint32(18).fork()).ldelim().ldelim()",u,i):n(".uint32(%i).%s(%s[ks[i]]).ldelim()",16|h,o,i),n("}")("}")):s.repeated?(n("if(%s!=null&&%s.length){",i,i),s.packed&&c.packed[o]!==g?n("w.uint32(%i).fork()",(s.id<<3|2)>>>0)("for(var i=0;i<%s.length;++i)",i)("w.%s(%s[i])",o,i)("w.ldelim()"):(n("for(var i=0;i<%s.length;++i)",i),h===g?l(n,s,u,i+"[i]"):n("w.uint32(%i).%s(%s[i])",(s.id<<3|h)>>>0,o,i)),n("}")):(s.optional&&n("if(%s!=null&&Object.hasOwnProperty.call(m,%j))",i,s.name),h===g?l(n,s,u,i):n("w.uint32(%i).%s(%s)",(s.id<<3|h)>>>0,o,i))}return n("return w")};var f=t(14),c=t(32),a=t(33);function l(t,i,n,r){i.resolvedType.group?t("types[%i].encode(%s,w.uint32(%i)).uint32(%i)",n,r,(i.id<<3|3)>>>0,(i.id<<3|4)>>>0):t("types[%i].encode(%s,w.uint32(%i).fork()).ldelim()",n,r,(i.id<<3|2)>>>0)}},{14:14,32:32,33:33}],14:[function(t,i,n){i.exports=s;var h=t(22),r=(((s.prototype=Object.create(h.prototype)).constructor=s).className="Enum",t(21)),e=t(33);function s(t,i,n,r,e,s){if(h.call(this,t,n),i&&"object"!=typeof i)throw TypeError("values must be an object");if(this.valuesById={},this.values=Object.create(this.valuesById),this.comment=r,this.comments=e||{},this.valuesOptions=s,this.reserved=g,i)for(var u=Object.keys(i),o=0;oi)return!0;return!1},a.isReservedName=function(t,i){if(t)for(var n=0;n "+t.len)}function h(t){this.buf=t,this.pos=0,this.len=t.length}function f(){return e.Buffer?function(t){return(h.create=function(t){return e.Buffer.isBuffer(t)?new r(t):a(t)})(t)}:a}var c,a="undefined"!=typeof Uint8Array?function(t){if(t instanceof Uint8Array||Array.isArray(t))return new h(t);throw Error("illegal buffer")}:function(t){if(Array.isArray(t))return new h(t);throw Error("illegal buffer")};function l(){var t=new s(0,0),i=0;if(!(4=this.len)throw o(this);if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t}return t.lo=(t.lo|(127&this.buf[this.pos++])<<7*i)>>>0,t}for(;i<4;++i)if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t;if(t.lo=(t.lo|(127&this.buf[this.pos])<<28)>>>0,t.hi=(t.hi|(127&this.buf[this.pos])>>4)>>>0,this.buf[this.pos++]<128)return t;if(i=0,4>>0,this.buf[this.pos++]<128)return t}else for(;i<5;++i){if(this.pos>=this.len)throw o(this);if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*i+3)>>>0,this.buf[this.pos++]<128)return t}throw Error("invalid varint encoding")}function d(t,i){return(t[i-4]|t[i-3]<<8|t[i-2]<<16|t[i-1]<<24)>>>0}function v(){if(this.pos+8>this.len)throw o(this,8);return new s(d(this.buf,this.pos+=4),d(this.buf,this.pos+=4))}h.create=f(),h.prototype.h=e.Array.prototype.subarray||e.Array.prototype.slice,h.prototype.uint32=(c=4294967295,function(){if(c=(127&this.buf[this.pos])>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<7)>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<14)>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<21)>>>0,this.buf[this.pos++]<128||(c=(c|(15&this.buf[this.pos])<<28)>>>0,this.buf[this.pos++]<128||!((this.pos+=5)>this.len))))))return c;throw this.pos=this.len,o(this,10)}),h.prototype.int32=function(){return 0|this.uint32()},h.prototype.sint32=function(){var t=this.uint32();return t>>>1^-(1&t)|0},h.prototype.bool=function(){return 0!==this.uint32()},h.prototype.fixed32=function(){if(this.pos+4>this.len)throw o(this,4);return d(this.buf,this.pos+=4)},h.prototype.sfixed32=function(){if(this.pos+4>this.len)throw o(this,4);return 0|d(this.buf,this.pos+=4)},h.prototype.float=function(){if(this.pos+4>this.len)throw o(this,4);var t=e.float.readFloatLE(this.buf,this.pos);return this.pos+=4,t},h.prototype.double=function(){if(this.pos+8>this.len)throw o(this,4);var t=e.float.readDoubleLE(this.buf,this.pos);return this.pos+=8,t},h.prototype.bytes=function(){var t=this.uint32(),i=this.pos,n=this.pos+t;if(n>this.len)throw o(this,t);return this.pos+=t,Array.isArray(this.buf)?this.buf.slice(i,n):i===n?(t=e.Buffer)?t.alloc(0):new this.buf.constructor(0):this.h.call(this.buf,i,n)},h.prototype.string=function(){var t=this.bytes();return u.read(t,0,t.length)},h.prototype.skip=function(t){if("number"==typeof t){if(this.pos+t>this.len)throw o(this,t);this.pos+=t}else do{if(this.pos>=this.len)throw o(this)}while(128&this.buf[this.pos++]);return this},h.prototype.skipType=function(t){switch(t){case 0:this.skip();break;case 1:this.skip(8);break;case 2:this.skip(this.uint32());break;case 3:for(;4!=(t=7&this.uint32());)this.skipType(t);break;case 5:this.skip(4);break;default:throw Error("invalid wire type "+t+" at offset "+this.pos)}return this},h.r=function(t){r=t,h.create=f(),r.r();var i=e.Long?"toLong":"toNumber";e.merge(h.prototype,{int64:function(){return l.call(this)[i](!1)},uint64:function(){return l.call(this)[i](!0)},sint64:function(){return l.call(this).zzDecode()[i](!1)},fixed64:function(){return v.call(this)[i](!0)},sfixed64:function(){return v.call(this)[i](!1)}})}},{35:35}],25:[function(t,i,n){i.exports=s;var r=t(24),e=((s.prototype=Object.create(r.prototype)).constructor=s,t(35));function s(t){r.call(this,t)}s.r=function(){e.Buffer&&(s.prototype.h=e.Buffer.prototype.slice)},s.prototype.string=function(){var t=this.uint32();return this.buf.utf8Slice?this.buf.utf8Slice(this.pos,this.pos=Math.min(this.pos+t,this.len)):this.buf.toString("utf-8",this.pos,this.pos=Math.min(this.pos+t,this.len))},s.r()},{24:24,35:35}],26:[function(t,i,n){i.exports=h;var r,d,v,e=t(21),s=(((h.prototype=Object.create(e.prototype)).constructor=h).className="Root",t(15)),u=t(14),o=t(23),b=t(33);function h(t){e.call(this,"",t),this.deferred=[],this.files=[]}function p(){}h.fromJSON=function(t,i){return i=i||new h,t.options&&i.setOptions(t.options),i.addJSON(t.nested)},h.prototype.resolvePath=b.path.resolve,h.prototype.fetch=b.fetch,h.prototype.load=function t(i,s,e){"function"==typeof s&&(e=s,s=g);var u=this;if(!e)return b.asPromise(t,u,i,s);var o=e===p;function h(t,i){if(e){if(o)throw t;var n=e;e=null,n(t,i)}}function f(t){var i=t.lastIndexOf("google/protobuf/");if(-1>>0,this.hi=i>>>0}var s=e.zero=new e(0,0),u=(s.toNumber=function(){return 0},s.zzEncode=s.zzDecode=function(){return this},s.length=function(){return 1},e.zeroHash="\0\0\0\0\0\0\0\0",e.fromNumber=function(t){var i,n;return 0===t?s:(n=(t=(i=t<0)?-t:t)>>>0,t=(t-n)/4294967296>>>0,i&&(t=~t>>>0,n=~n>>>0,4294967295<++n&&(n=0,4294967295<++t&&(t=0))),new e(n,t))},e.from=function(t){if("number"==typeof t)return e.fromNumber(t);if(r.isString(t)){if(!r.Long)return e.fromNumber(parseInt(t,10));t=r.Long.fromString(t)}return t.low||t.high?new e(t.low>>>0,t.high>>>0):s},e.prototype.toNumber=function(t){var i;return!t&&this.hi>>>31?(t=1+~this.lo>>>0,i=~this.hi>>>0,-(t+4294967296*(i=t?i:i+1>>>0))):this.lo+4294967296*this.hi},e.prototype.toLong=function(t){return r.Long?new r.Long(0|this.lo,0|this.hi,!!t):{low:0|this.lo,high:0|this.hi,unsigned:!!t}},String.prototype.charCodeAt);e.fromHash=function(t){return"\0\0\0\0\0\0\0\0"===t?s:new e((u.call(t,0)|u.call(t,1)<<8|u.call(t,2)<<16|u.call(t,3)<<24)>>>0,(u.call(t,4)|u.call(t,5)<<8|u.call(t,6)<<16|u.call(t,7)<<24)>>>0)},e.prototype.toHash=function(){return String.fromCharCode(255&this.lo,this.lo>>>8&255,this.lo>>>16&255,this.lo>>>24,255&this.hi,this.hi>>>8&255,this.hi>>>16&255,this.hi>>>24)},e.prototype.zzEncode=function(){var t=this.hi>>31;return this.hi=((this.hi<<1|this.lo>>>31)^t)>>>0,this.lo=(this.lo<<1^t)>>>0,this},e.prototype.zzDecode=function(){var t=-(1&this.lo);return this.lo=((this.lo>>>1|this.hi<<31)^t)>>>0,this.hi=(this.hi>>>1^t)>>>0,this},e.prototype.length=function(){var t=this.lo,i=(this.lo>>>28|this.hi<<4)>>>0,n=this.hi>>>24;return 0==n?0==i?t<16384?t<128?1:2:t<2097152?3:4:i<16384?i<128?5:6:i<2097152?7:8:n<128?9:10}},{35:35}],35:[function(t,i,n){var r=n;function e(t,i,n){for(var r=Object.keys(i),e=0;e>>7|t.hi<<25)>>>0,t.hi>>>=7;for(;127>>7;i[n++]=t.lo}function p(t,i,n){i[n]=255&t,i[n+1]=t>>>8&255,i[n+2]=t>>>16&255,i[n+3]=t>>>24}a.create=l(),a.alloc=function(t){return new e.Array(t)},e.Array!==Array&&(a.alloc=e.pool(a.alloc,e.Array.prototype.subarray)),a.prototype.p=function(t,i,n){return this.tail=this.tail.next=new h(t,i,n),this.len+=i,this},(v.prototype=Object.create(h.prototype)).fn=function(t,i,n){for(;127>>=7;i[n]=t},a.prototype.uint32=function(t){return this.len+=(this.tail=this.tail.next=new v((t>>>=0)<128?1:t<16384?2:t<2097152?3:t<268435456?4:5,t)).len,this},a.prototype.int32=function(t){return t<0?this.p(b,10,s.fromNumber(t)):this.uint32(t)},a.prototype.sint32=function(t){return this.uint32((t<<1^t>>31)>>>0)},a.prototype.int64=a.prototype.uint64=function(t){t=s.from(t);return this.p(b,t.length(),t)},a.prototype.sint64=function(t){t=s.from(t).zzEncode();return this.p(b,t.length(),t)},a.prototype.bool=function(t){return this.p(d,1,t?1:0)},a.prototype.sfixed32=a.prototype.fixed32=function(t){return this.p(p,4,t>>>0)},a.prototype.sfixed64=a.prototype.fixed64=function(t){t=s.from(t);return this.p(p,4,t.lo).p(p,4,t.hi)},a.prototype.float=function(t){return this.p(e.float.writeFloatLE,4,t)},a.prototype.double=function(t){return this.p(e.float.writeDoubleLE,8,t)};var y=e.Array.prototype.set?function(t,i,n){i.set(t,n)}:function(t,i,n){for(var r=0;r>>0;return n?(e.isString(t)&&(i=a.alloc(n=u.length(t)),u.decode(t,i,0),t=i),this.uint32(n).p(y,n,t)):this.p(d,1,0)},a.prototype.string=function(t){var i=o.length(t);return i?this.uint32(i).p(o.write,i,t):this.p(d,1,0)},a.prototype.fork=function(){return this.states=new c(this),this.head=this.tail=new h(f,0,0),this.len=0,this},a.prototype.reset=function(){return this.states?(this.head=this.states.head,this.tail=this.states.tail,this.len=this.states.len,this.states=this.states.next):(this.head=this.tail=new h(f,0,0),this.len=0),this},a.prototype.ldelim=function(){var t=this.head,i=this.tail,n=this.len;return this.reset().uint32(n),n&&(this.tail.next=t.next,this.tail=i,this.len+=n),this},a.prototype.finish=function(){for(var t=this.head.next,i=this.constructor.alloc(this.len),n=0;t;)t.fn(t.val,i,n),n+=t.len,t=t.next;return i},a.r=function(t){r=t,a.create=l(),r.r()}},{35:35}],39:[function(t,i,n){i.exports=s;var r=t(38),e=((s.prototype=Object.create(r.prototype)).constructor=s,t(35));function s(){r.call(this)}function u(t,i,n){t.length<40?e.utf8.write(t,i,n):i.utf8Write?i.utf8Write(t,n):i.write(t,n)}s.r=function(){s.alloc=e.b,s.writeBytesBuffer=e.Buffer&&e.Buffer.prototype instanceof Uint8Array&&"set"===e.Buffer.prototype.set.name?function(t,i,n){i.set(t,n)}:function(t,i,n){if(t.copy)t.copy(i,n,0,t.length);else for(var r=0;r>>0;return this.uint32(i),i&&this.p(s.writeBytesBuffer,i,t),this},s.prototype.string=function(t){var i=e.Buffer.byteLength(t);return this.uint32(i),i&&this.p(u,i,t),this},s.r()},{35:35,38:38}]},{},[16])}(); -+!function(g){"use strict";!function(r,e,t){var i=function t(i){var n=e[i];return n||r[i][0].call(n=e[i]={exports:{}},t,n,n.exports),n.exports}(t[0]);i.util.global.protobuf=i,"function"==typeof define&&define.amd&&define(["long"],function(t){return t&&t.isLong&&(i.util.Long=t,i.configure()),i}),"object"==typeof module&&module&&module.exports&&(module.exports=i)}({1:[function(t,i,n){i.exports=function(t,i){var n=Array(arguments.length-1),s=0,r=2,o=!0;for(;r>2],r=(3&h)<<4,u=1;break;case 1:s[o++]=f[r|h>>4],r=(15&h)<<2,u=2;break;case 2:s[o++]=f[r|h>>6],s[o++]=f[63&h],u=0}8191>4,r=u,s=2;break;case 2:i[n++]=(15&r)<<4|(60&u)>>2,r=u,s=3;break;case 3:i[n++]=(3&r)<<6|u,s=0}}if(1===s)throw Error(c);return n-e},n.test=function(t){return/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(t)}},{}],3:[function(t,i,n){function a(i,n){"string"==typeof i&&(n=i,i=g);var h=[];function f(t){if("string"!=typeof t){var i=c();if(a.verbose&&console.log("codegen: "+i),i="return "+i,t){for(var n=Object.keys(t),r=Array(n.length+1),e=Array(n.length),s=0;s>>0:i<11754943508222875e-54?(e<<31|Math.round(i/1401298464324817e-60))>>>0:(e<<31|127+(t=Math.floor(Math.log(i)/Math.LN2))<<23|8388607&Math.round(i*Math.pow(2,-t)*8388608))>>>0,n,r)}function n(t,i,n){t=t(i,n),i=2*(t>>31)+1,n=t>>>23&255,t&=8388607;return 255==n?t?NaN:1/0*i:0==n?1401298464324817e-60*i*t:i*Math.pow(2,n-150)*(8388608+t)}function r(t,i,n){u[0]=t,i[n]=h[0],i[n+1]=h[1],i[n+2]=h[2],i[n+3]=h[3]}function e(t,i,n){u[0]=t,i[n]=h[3],i[n+1]=h[2],i[n+2]=h[1],i[n+3]=h[0]}function s(t,i){return h[0]=t[i],h[1]=t[i+1],h[2]=t[i+2],h[3]=t[i+3],u[0]}function o(t,i){return h[3]=t[i],h[2]=t[i+1],h[1]=t[i+2],h[0]=t[i+3],u[0]}var u,h,f,c,a;function l(t,i,n,r,e,s){var o,u=r<0?1:0;0===(r=u?-r:r)?(t(0,e,s+i),t(0<1/r?0:2147483648,e,s+n)):isNaN(r)?(t(0,e,s+i),t(2146959360,e,s+n)):17976931348623157e292>>0,e,s+n)):r<22250738585072014e-324?(t((o=r/5e-324)>>>0,e,s+i),t((u<<31|o/4294967296)>>>0,e,s+n)):(t(4503599627370496*(o=r*Math.pow(2,-(r=1024===(r=Math.floor(Math.log(r)/Math.LN2))?1023:r)))>>>0,e,s+i),t((u<<31|r+1023<<20|1048576*o&1048575)>>>0,e,s+n))}function d(t,i,n,r,e){i=t(r,e+i),t=t(r,e+n),r=2*(t>>31)+1,e=t>>>20&2047,n=4294967296*(1048575&t)+i;return 2047==e?n?NaN:1/0*r:0==e?5e-324*r*n:r*Math.pow(2,e-1075)*(n+4503599627370496)}function v(t,i,n){f[0]=t,i[n]=c[0],i[n+1]=c[1],i[n+2]=c[2],i[n+3]=c[3],i[n+4]=c[4],i[n+5]=c[5],i[n+6]=c[6],i[n+7]=c[7]}function b(t,i,n){f[0]=t,i[n]=c[7],i[n+1]=c[6],i[n+2]=c[5],i[n+3]=c[4],i[n+4]=c[3],i[n+5]=c[2],i[n+6]=c[1],i[n+7]=c[0]}function p(t,i){return c[0]=t[i],c[1]=t[i+1],c[2]=t[i+2],c[3]=t[i+3],c[4]=t[i+4],c[5]=t[i+5],c[6]=t[i+6],c[7]=t[i+7],f[0]}function y(t,i){return c[7]=t[i],c[6]=t[i+1],c[5]=t[i+2],c[4]=t[i+3],c[3]=t[i+4],c[2]=t[i+5],c[1]=t[i+6],c[0]=t[i+7],f[0]}return"undefined"!=typeof Float32Array?(u=new Float32Array([-0]),h=new Uint8Array(u.buffer),a=128===h[3],t.writeFloatLE=a?r:e,t.writeFloatBE=a?e:r,t.readFloatLE=a?s:o,t.readFloatBE=a?o:s):(t.writeFloatLE=i.bind(null,w),t.writeFloatBE=i.bind(null,m),t.readFloatLE=n.bind(null,g),t.readFloatBE=n.bind(null,j)),"undefined"!=typeof Float64Array?(f=new Float64Array([-0]),c=new Uint8Array(f.buffer),a=128===c[7],t.writeDoubleLE=a?v:b,t.writeDoubleBE=a?b:v,t.readDoubleLE=a?p:y,t.readDoubleBE=a?y:p):(t.writeDoubleLE=l.bind(null,w,0,4),t.writeDoubleBE=l.bind(null,m,4,0),t.readDoubleLE=d.bind(null,g,0,4),t.readDoubleBE=d.bind(null,j,4,0)),t}function w(t,i,n){i[n]=255&t,i[n+1]=t>>>8&255,i[n+2]=t>>>16&255,i[n+3]=t>>>24}function m(t,i,n){i[n]=t>>>24,i[n+1]=t>>>16&255,i[n+2]=t>>>8&255,i[n+3]=255&t}function g(t,i){return(t[i]|t[i+1]<<8|t[i+2]<<16|t[i+3]<<24)>>>0}function j(t,i){return(t[i]<<24|t[i+1]<<16|t[i+2]<<8|t[i+3])>>>0}i.exports=r(r)},{}],7:[function(t,i,n){function r(t){try{var i=eval("require")(t);if(i&&(i.length||Object.keys(i).length))return i}catch(t){}return null}i.exports=r},{}],8:[function(t,i,n){var e=n.isAbsolute=function(t){return/^(?:\/|\w+:)/.test(t)},r=n.normalize=function(t){var i=(t=t.replace(/\\/g,"/").replace(/\/{2,}/g,"/")).split("/"),n=e(t),t="";n&&(t=i.shift()+"/");for(var r=0;r>>1,s=null,o=r;return function(t){if(t<1||e>10),s[o++]=56320+(1023&r)):s[o++]=(15&r)<<12|(63&t[i++])<<6|63&t[i++],8191>6|192:(55296==(64512&r)&&56320==(64512&(e=t.charCodeAt(o+1)))?(++o,i[n++]=(r=65536+((1023&r)<<10)+(1023&e))>>18|240,i[n++]=r>>12&63|128):i[n++]=r>>12|224,i[n++]=r>>6&63|128),i[n++]=63&r|128);return n-s}},{}],11:[function(t,i,n){var l=t(14),d=t(33);function o(t,i,n,r){var e=!1;if(i.resolvedType)if(i.resolvedType instanceof l){t("switch(d%s){",r);for(var s=i.resolvedType.values,o=Object.keys(s),u=0;u>>0",r,r);break;case"int32":case"sint32":case"sfixed32":t("m%s=d%s|0",r,r);break;case"uint64":h=!0;case"int64":case"sint64":case"fixed64":case"sfixed64":t("if(util.Long)")("(m%s=util.Long.fromValue(d%s)).unsigned=%j",r,r,h)('else if(typeof d%s==="string")',r)("m%s=parseInt(d%s,10)",r,r)('else if(typeof d%s==="number")',r)("m%s=d%s",r,r)('else if(typeof d%s==="object")',r)("m%s=new util.LongBits(d%s.low>>>0,d%s.high>>>0).toNumber(%s)",r,r,r,h?"true":"");break;case"bytes":t('if(typeof d%s==="string")',r)("util.base64.decode(d%s,m%s=util.newBuffer(util.base64.length(d%s)),0)",r,r,r)("else if(d%s.length >= 0)",r)("m%s=d%s",r,r);break;case"string":t("m%s=String(d%s)",r,r);break;case"bool":t("m%s=Boolean(d%s)",r,r)}}return t}function v(t,i,n,r){if(i.resolvedType)i.resolvedType instanceof l?t("d%s=o.enums===String?(types[%i].values[m%s]===undefined?m%s:types[%i].values[m%s]):m%s",r,n,r,r,n,r,r):t("d%s=types[%i].toObject(m%s,o)",r,n,r);else{var e=!1;switch(i.type){case"double":case"float":t("d%s=o.json&&!isFinite(m%s)?String(m%s):m%s",r,r,r,r);break;case"uint64":e=!0;case"int64":case"sint64":case"fixed64":case"sfixed64":t('if(typeof m%s==="number")',r)("d%s=o.longs===String?String(m%s):m%s",r,r,r)("else")("d%s=o.longs===String?util.Long.prototype.toString.call(m%s):o.longs===Number?new util.LongBits(m%s.low>>>0,m%s.high>>>0).toNumber(%s):m%s",r,r,r,r,e?"true":"",r);break;case"bytes":t("d%s=o.bytes===String?util.base64.encode(m%s,0,m%s.length):o.bytes===Array?Array.prototype.slice.call(m%s):m%s",r,r,r,r,r);break;default:t("d%s=m%s",r,r)}}return t}n.fromObject=function(t){var i=t.fieldsArray,n=d.codegen(["d"],t.name+"$fromObject")("if(d instanceof this.ctor)")("return d");if(!i.length)return n("return new this.ctor");n("var m=new this.ctor");for(var r=0;r>>3){");for(var n="m"+c.safeProp("$unknownFields"),r=0;r>>3){")("case 1: k=r.%s(); break",e.keyType)("case 2:"),f.basic[s]===g?i("value=types[%i].decode(r,r.uint32())",r):i("value=r.%s()",s),i("break")("default:")("r.skipType(tag2&7)")("break")("}")("}"),f.long[e.keyType]!==g?i('%s[typeof k==="object"?util.longToHash(k):k]=value',o):i("%s[k]=value",o)):e.repeated?(i("if(!(%s&&%s.length))",o,o)("%s=[]",o),f.packed[s]!==g&&i("if((t&7)===2){")("var c2=r.uint32()+r.pos")("while(r.pos>>0,8|a.mapKey[o.keyType],o.keyType),f===g?n("types[%i].encode(%s[ks[i]],w.uint32(18).fork()).ldelim().ldelim()",u,i):n(".uint32(%i).%s(%s[ks[i]]).ldelim()",16|f,h,i),n("}")("}")):o.repeated?(n("if(%s!=null&&%s.length){",i,i),o.packed&&a.packed[h]!==g?n("w.uint32(%i).fork()",(o.id<<3|2)>>>0)("for(var i=0;i<%s.length;++i)",i)("w.%s(%s[i])",h,i)("w.ldelim()"):(n("for(var i=0;i<%s.length;++i)",i),f===g?d(n,o,u,i+"[i]"):n("w.uint32(%i).%s(%s[i])",(o.id<<3|f)>>>0,h,i)),n("}")):(o.optional&&n("if(%s!=null&&Object.hasOwnProperty.call(m,%j))",i,o.name),f===g?d(n,o,u,i):n("w.uint32(%i).%s(%s)",(o.id<<3|f)>>>0,h,i))}return n("for(var i=0;i>>0,(i.id<<3|4)>>>0):t("types[%i].encode(%s,w.uint32(%i).fork()).ldelim()",n,r,(i.id<<3|2)>>>0)}},{14:14,32:32,33:33}],14:[function(t,i,n){i.exports=s;var h=t(22),r=(((s.prototype=Object.create(h.prototype)).constructor=s).className="Enum",t(21)),e=t(33);function s(t,i,n,r,e,s){if(h.call(this,t,n),i&&"object"!=typeof i)throw TypeError("values must be an object");if(this.valuesById={},this.values=Object.create(this.valuesById),this.comment=r,this.comments=e||{},this.valuesOptions=s,this.reserved=g,i)for(var o=Object.keys(i),u=0;ui)return!0;return!1},a.isReservedName=function(t,i){if(t)for(var n=0;n "+t.len)}function h(t){this.buf=t,this.pos=0,this.len=t.length}function f(){return e.Buffer?function(t){return(h.create=function(t){return e.Buffer.isBuffer(t)?new r(t):a(t)})(t)}:a}var c,a="undefined"!=typeof Uint8Array?function(t){if(t instanceof Uint8Array||Array.isArray(t))return new h(t);throw Error("illegal buffer")}:function(t){if(Array.isArray(t))return new h(t);throw Error("illegal buffer")};function l(){var t=new s(0,0),i=0;if(!(4=this.len)throw u(this);if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t}return t.lo=(t.lo|(127&this.buf[this.pos++])<<7*i)>>>0,t}for(;i<4;++i)if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t;if(t.lo=(t.lo|(127&this.buf[this.pos])<<28)>>>0,t.hi=(t.hi|(127&this.buf[this.pos])>>4)>>>0,this.buf[this.pos++]<128)return t;if(i=0,4>>0,this.buf[this.pos++]<128)return t}else for(;i<5;++i){if(this.pos>=this.len)throw u(this);if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*i+3)>>>0,this.buf[this.pos++]<128)return t}throw Error("invalid varint encoding")}function d(t,i){return(t[i-4]|t[i-3]<<8|t[i-2]<<16|t[i-1]<<24)>>>0}function v(){if(this.pos+8>this.len)throw u(this,8);return new s(d(this.buf,this.pos+=4),d(this.buf,this.pos+=4))}h.create=f(),h.prototype.h=e.Array.prototype.subarray||e.Array.prototype.slice,h.prototype.uint32=(c=4294967295,function(){if(c=(127&this.buf[this.pos])>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<7)>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<14)>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<21)>>>0,this.buf[this.pos++]<128||(c=(c|(15&this.buf[this.pos])<<28)>>>0,this.buf[this.pos++]<128||!((this.pos+=5)>this.len))))))return c;throw this.pos=this.len,u(this,10)}),h.prototype.int32=function(){return 0|this.uint32()},h.prototype.sint32=function(){var t=this.uint32();return t>>>1^-(1&t)|0},h.prototype.bool=function(){return 0!==this.uint32()},h.prototype.fixed32=function(){if(this.pos+4>this.len)throw u(this,4);return d(this.buf,this.pos+=4)},h.prototype.sfixed32=function(){if(this.pos+4>this.len)throw u(this,4);return 0|d(this.buf,this.pos+=4)},h.prototype.float=function(){if(this.pos+4>this.len)throw u(this,4);var t=e.float.readFloatLE(this.buf,this.pos);return this.pos+=4,t},h.prototype.double=function(){if(this.pos+8>this.len)throw u(this,4);var t=e.float.readDoubleLE(this.buf,this.pos);return this.pos+=8,t},h.prototype.bytes=function(){var t=this.uint32(),i=this.pos,n=this.pos+t;if(n>this.len)throw u(this,t);return this.pos+=t,Array.isArray(this.buf)?this.buf.slice(i,n):i===n?(t=e.Buffer)?t.alloc(0):new this.buf.constructor(0):this.h.call(this.buf,i,n)},h.prototype.string=function(){var t=this.bytes();return o.read(t,0,t.length)},h.prototype.skip=function(t){if("number"==typeof t){if(this.pos+t>this.len)throw u(this,t);this.pos+=t}else do{if(this.pos>=this.len)throw u(this)}while(128&this.buf[this.pos++]);return this},h.prototype.skipType=function(t){switch(t){case 0:this.skip();break;case 1:this.skip(8);break;case 2:this.skip(this.uint32());break;case 3:for(;4!=(t=7&this.uint32());)this.skipType(t);break;case 5:this.skip(4);break;default:throw Error("invalid wire type "+t+" at offset "+this.pos)}return this},h.r=function(t){r=t,h.create=f(),r.r();var i=e.Long?"toLong":"toNumber";e.merge(h.prototype,{int64:function(){return l.call(this)[i](!1)},uint64:function(){return l.call(this)[i](!0)},sint64:function(){return l.call(this).zzDecode()[i](!1)},fixed64:function(){return v.call(this)[i](!0)},sfixed64:function(){return v.call(this)[i](!1)}})}},{35:35}],25:[function(t,i,n){i.exports=s;var r=t(24),e=((s.prototype=Object.create(r.prototype)).constructor=s,t(35));function s(t){r.call(this,t)}s.r=function(){e.Buffer&&(s.prototype.h=e.Buffer.prototype.slice)},s.prototype.string=function(){var t=this.uint32();return this.buf.utf8Slice?this.buf.utf8Slice(this.pos,this.pos=Math.min(this.pos+t,this.len)):this.buf.toString("utf-8",this.pos,this.pos=Math.min(this.pos+t,this.len))},s.r()},{24:24,35:35}],26:[function(t,i,n){i.exports=h;var r,d,v,e=t(21),s=(((h.prototype=Object.create(e.prototype)).constructor=h).className="Root",t(15)),o=t(14),u=t(23),b=t(33);function h(t){e.call(this,"",t),this.deferred=[],this.files=[]}function p(){}h.fromJSON=function(t,i){return i=i||new h,t.options&&i.setOptions(t.options),i.addJSON(t.nested)},h.prototype.resolvePath=b.path.resolve,h.prototype.fetch=b.fetch,h.prototype.load=function t(i,s,e){"function"==typeof s&&(e=s,s=g);var o=this;if(!e)return b.asPromise(t,o,i,s);var u=e===p;function h(t,i){if(e){if(u)throw t;var n=e;e=null,n(t,i)}}function f(t){var i=t.lastIndexOf("google/protobuf/");if(-1>>0,this.hi=i>>>0}var s=e.zero=new e(0,0),o=(s.toNumber=function(){return 0},s.zzEncode=s.zzDecode=function(){return this},s.length=function(){return 1},e.zeroHash="\0\0\0\0\0\0\0\0",e.fromNumber=function(t){var i,n;return 0===t?s:(n=(t=(i=t<0)?-t:t)>>>0,t=(t-n)/4294967296>>>0,i&&(t=~t>>>0,n=~n>>>0,4294967295<++n&&(n=0,4294967295<++t&&(t=0))),new e(n,t))},e.from=function(t){if("number"==typeof t)return e.fromNumber(t);if(r.isString(t)){if(!r.Long)return e.fromNumber(parseInt(t,10));t=r.Long.fromString(t)}return t.low||t.high?new e(t.low>>>0,t.high>>>0):s},e.prototype.toNumber=function(t){var i;return!t&&this.hi>>>31?(t=1+~this.lo>>>0,i=~this.hi>>>0,-(t+4294967296*(i=t?i:i+1>>>0))):this.lo+4294967296*this.hi},e.prototype.toLong=function(t){return r.Long?new r.Long(0|this.lo,0|this.hi,!!t):{low:0|this.lo,high:0|this.hi,unsigned:!!t}},String.prototype.charCodeAt);e.fromHash=function(t){return"\0\0\0\0\0\0\0\0"===t?s:new e((o.call(t,0)|o.call(t,1)<<8|o.call(t,2)<<16|o.call(t,3)<<24)>>>0,(o.call(t,4)|o.call(t,5)<<8|o.call(t,6)<<16|o.call(t,7)<<24)>>>0)},e.prototype.toHash=function(){return String.fromCharCode(255&this.lo,this.lo>>>8&255,this.lo>>>16&255,this.lo>>>24,255&this.hi,this.hi>>>8&255,this.hi>>>16&255,this.hi>>>24)},e.prototype.zzEncode=function(){var t=this.hi>>31;return this.hi=((this.hi<<1|this.lo>>>31)^t)>>>0,this.lo=(this.lo<<1^t)>>>0,this},e.prototype.zzDecode=function(){var t=-(1&this.lo);return this.lo=((this.lo>>>1|this.hi<<31)^t)>>>0,this.hi=(this.hi>>>1^t)>>>0,this},e.prototype.length=function(){var t=this.lo,i=(this.lo>>>28|this.hi<<4)>>>0,n=this.hi>>>24;return 0==n?0==i?t<16384?t<128?1:2:t<2097152?3:4:i<16384?i<128?5:6:i<2097152?7:8:n<128?9:10}},{35:35}],35:[function(t,i,n){var r=n;function e(t,i,n){for(var r=Object.keys(i),e=0;e>>7|t.hi<<25)>>>0,t.hi>>>=7;for(;127>>7;i[n++]=t.lo}function p(t,i,n){i[n]=255&t,i[n+1]=t>>>8&255,i[n+2]=t>>>16&255,i[n+3]=t>>>24}a.create=l(),a.alloc=function(t){return new e.Array(t)},e.Array!==Array&&(a.alloc=e.pool(a.alloc,e.Array.prototype.subarray)),a.prototype.p=function(t,i,n){return this.tail=this.tail.next=new h(t,i,n),this.len+=i,this},(v.prototype=Object.create(h.prototype)).fn=function(t,i,n){for(;127>>=7;i[n]=t},a.prototype.uint32=function(t){return this.len+=(this.tail=this.tail.next=new v((t>>>=0)<128?1:t<16384?2:t<2097152?3:t<268435456?4:5,t)).len,this},a.prototype.int32=function(t){return t<0?this.p(b,10,s.fromNumber(t)):this.uint32(t)},a.prototype.sint32=function(t){return this.uint32((t<<1^t>>31)>>>0)},a.prototype.int64=a.prototype.uint64=function(t){t=s.from(t);return this.p(b,t.length(),t)},a.prototype.sint64=function(t){t=s.from(t).zzEncode();return this.p(b,t.length(),t)},a.prototype.bool=function(t){return this.p(d,1,t?1:0)},a.prototype.sfixed32=a.prototype.fixed32=function(t){return this.p(p,4,t>>>0)},a.prototype.sfixed64=a.prototype.fixed64=function(t){t=s.from(t);return this.p(p,4,t.lo).p(p,4,t.hi)},a.prototype.float=function(t){return this.p(e.float.writeFloatLE,4,t)},a.prototype.double=function(t){return this.p(e.float.writeDoubleLE,8,t)};var y=e.Array.prototype.set?function(t,i,n){i.set(t,n)}:function(t,i,n){for(var r=0;r>>0;return n?(e.isString(t)&&(i=a.alloc(n=o.length(t)),o.decode(t,i,0),t=i),this.uint32(n).p(y,n,t)):this.p(d,1,0)},a.prototype.y=function(t){return this.p(y,t.length,t)},a.prototype.string=function(t){var i=u.length(t);return i?this.uint32(i).p(u.write,i,t):this.p(d,1,0)},a.prototype.fork=function(){return this.states=new c(this),this.head=this.tail=new h(f,0,0),this.len=0,this},a.prototype.reset=function(){return this.states?(this.head=this.states.head,this.tail=this.states.tail,this.len=this.states.len,this.states=this.states.next):(this.head=this.tail=new h(f,0,0),this.len=0),this},a.prototype.ldelim=function(){var t=this.head,i=this.tail,n=this.len;return this.reset().uint32(n),n&&(this.tail.next=t.next,this.tail=i,this.len+=n),this},a.prototype.finish=function(){for(var t=this.head.next,i=this.constructor.alloc(this.len),n=0;t;)t.fn(t.val,i,n),n+=t.len,t=t.next;return i},a.r=function(t){r=t,a.create=l(),r.r()}},{35:35}],39:[function(t,i,n){i.exports=s;var r=t(38),e=((s.prototype=Object.create(r.prototype)).constructor=s,t(35));function s(){r.call(this)}function o(t,i,n){t.length<40?e.utf8.write(t,i,n):i.utf8Write?i.utf8Write(t,n):i.write(t,n)}s.r=function(){s.alloc=e.b,s.writeBytesBuffer=e.Buffer&&e.Buffer.prototype instanceof Uint8Array&&"set"===e.Buffer.prototype.set.name?function(t,i,n){i.set(t,n)}:function(t,i,n){if(t.copy)t.copy(i,n,0,t.length);else for(var r=0;r>>0;return this.uint32(i),i&&this.p(s.writeBytesBuffer,i,t),this},s.prototype.string=function(t){var i=e.Buffer.byteLength(t);return this.uint32(i),i&&this.p(o,i,t),this},s.r()},{35:35,38:38}]},{},[16])}(); - //# sourceMappingURL=protobuf.min.js.map -diff --git a/dist/minimal/protobuf.js b/dist/minimal/protobuf.js -index 263f96e..748e63f 100644 ---- a/dist/minimal/protobuf.js -+++ b/dist/minimal/protobuf.js -@@ -1,6 +1,6 @@ - /*! - * protobuf.js v7.3.2 (c) 2016, daniel wirtz -- * compiled wed, 12 jun 2024 08:24:21 utc -+ * compiled mon, 24 jun 2024 23:18:03 utc - * licensed under the bsd-3-clause license - * see: https://github.com/dcodeio/protobuf.js for details - */ -@@ -2562,6 +2562,11 @@ Writer.prototype.bytes = function write_bytes(value) { - return this.uint32(len)._push(writeBytes, len, value); - }; - -+ -+Writer.prototype._unknownField = function __unknownField(field) { -+ return this._push(writeBytes, field.length, field); -+}; -+ - /** - * Writes a string. - * @param {string} value Value to write -diff --git a/dist/minimal/protobuf.min.js b/dist/minimal/protobuf.min.js -index 1841eae..ad583eb 100644 ---- a/dist/minimal/protobuf.min.js -+++ b/dist/minimal/protobuf.min.js -@@ -1,8 +1,8 @@ - /*! - * protobuf.js v7.3.2 (c) 2016, daniel wirtz -- * compiled wed, 12 jun 2024 08:24:21 utc -+ * compiled mon, 24 jun 2024 23:18:03 utc - * licensed under the bsd-3-clause license - * see: https://github.com/dcodeio/protobuf.js for details - */ --!function(d){"use strict";!function(r,u,t){var n=function t(n){var i=u[n];return i||r[n][0].call(i=u[n]={exports:{}},t,i,i.exports),i.exports}(t[0]);n.util.global.protobuf=n,"function"==typeof define&&define.amd&&define(["long"],function(t){return t&&t.isLong&&(n.util.Long=t,n.configure()),n}),"object"==typeof module&&module&&module.exports&&(module.exports=n)}({1:[function(t,n,i){n.exports=function(t,n){var i=Array(arguments.length-1),e=0,r=2,s=!0;for(;r>2],r=(3&o)<<4,h=1;break;case 1:e[s++]=f[r|o>>4],r=(15&o)<<2,h=2;break;case 2:e[s++]=f[r|o>>6],e[s++]=f[63&o],h=0}8191>4,r=h,e=2;break;case 2:n[i++]=(15&r)<<4|(60&h)>>2,r=h,e=3;break;case 3:n[i++]=(3&r)<<6|h,e=0}}if(1===e)throw Error(c);return i-u},i.test=function(t){return/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(t)}},{}],3:[function(t,n,i){function r(){this.t={}}(n.exports=r).prototype.on=function(t,n,i){return(this.t[t]||(this.t[t]=[])).push({fn:n,ctx:i||this}),this},r.prototype.off=function(t,n){if(t===d)this.t={};else if(n===d)this.t[t]=[];else for(var i=this.t[t],r=0;r>>0:n<11754943508222875e-54?(u<<31|Math.round(n/1401298464324817e-60))>>>0:(u<<31|127+(t=Math.floor(Math.log(n)/Math.LN2))<<23|8388607&Math.round(n*Math.pow(2,-t)*8388608))>>>0,i,r)}function i(t,n,i){t=t(n,i),n=2*(t>>31)+1,i=t>>>23&255,t&=8388607;return 255==i?t?NaN:1/0*n:0==i?1401298464324817e-60*n*t:n*Math.pow(2,i-150)*(8388608+t)}function r(t,n,i){h[0]=t,n[i]=o[0],n[i+1]=o[1],n[i+2]=o[2],n[i+3]=o[3]}function u(t,n,i){h[0]=t,n[i]=o[3],n[i+1]=o[2],n[i+2]=o[1],n[i+3]=o[0]}function e(t,n){return o[0]=t[n],o[1]=t[n+1],o[2]=t[n+2],o[3]=t[n+3],h[0]}function s(t,n){return o[3]=t[n],o[2]=t[n+1],o[1]=t[n+2],o[0]=t[n+3],h[0]}var h,o,f,c,a;function l(t,n,i,r,u,e){var s,h=r<0?1:0;0===(r=h?-r:r)?(t(0,u,e+n),t(0<1/r?0:2147483648,u,e+i)):isNaN(r)?(t(0,u,e+n),t(2146959360,u,e+i)):17976931348623157e292>>0,u,e+i)):r<22250738585072014e-324?(t((s=r/5e-324)>>>0,u,e+n),t((h<<31|s/4294967296)>>>0,u,e+i)):(t(4503599627370496*(s=r*Math.pow(2,-(r=1024===(r=Math.floor(Math.log(r)/Math.LN2))?1023:r)))>>>0,u,e+n),t((h<<31|r+1023<<20|1048576*s&1048575)>>>0,u,e+i))}function v(t,n,i,r,u){n=t(r,u+n),t=t(r,u+i),r=2*(t>>31)+1,u=t>>>20&2047,i=4294967296*(1048575&t)+n;return 2047==u?i?NaN:1/0*r:0==u?5e-324*r*i:r*Math.pow(2,u-1075)*(i+4503599627370496)}function w(t,n,i){f[0]=t,n[i]=c[0],n[i+1]=c[1],n[i+2]=c[2],n[i+3]=c[3],n[i+4]=c[4],n[i+5]=c[5],n[i+6]=c[6],n[i+7]=c[7]}function b(t,n,i){f[0]=t,n[i]=c[7],n[i+1]=c[6],n[i+2]=c[5],n[i+3]=c[4],n[i+4]=c[3],n[i+5]=c[2],n[i+6]=c[1],n[i+7]=c[0]}function y(t,n){return c[0]=t[n],c[1]=t[n+1],c[2]=t[n+2],c[3]=t[n+3],c[4]=t[n+4],c[5]=t[n+5],c[6]=t[n+6],c[7]=t[n+7],f[0]}function g(t,n){return c[7]=t[n],c[6]=t[n+1],c[5]=t[n+2],c[4]=t[n+3],c[3]=t[n+4],c[2]=t[n+5],c[1]=t[n+6],c[0]=t[n+7],f[0]}return"undefined"!=typeof Float32Array?(h=new Float32Array([-0]),o=new Uint8Array(h.buffer),a=128===o[3],t.writeFloatLE=a?r:u,t.writeFloatBE=a?u:r,t.readFloatLE=a?e:s,t.readFloatBE=a?s:e):(t.writeFloatLE=n.bind(null,d),t.writeFloatBE=n.bind(null,A),t.readFloatLE=i.bind(null,p),t.readFloatBE=i.bind(null,m)),"undefined"!=typeof Float64Array?(f=new Float64Array([-0]),c=new Uint8Array(f.buffer),a=128===c[7],t.writeDoubleLE=a?w:b,t.writeDoubleBE=a?b:w,t.readDoubleLE=a?y:g,t.readDoubleBE=a?g:y):(t.writeDoubleLE=l.bind(null,d,0,4),t.writeDoubleBE=l.bind(null,A,4,0),t.readDoubleLE=v.bind(null,p,0,4),t.readDoubleBE=v.bind(null,m,4,0)),t}function d(t,n,i){n[i]=255&t,n[i+1]=t>>>8&255,n[i+2]=t>>>16&255,n[i+3]=t>>>24}function A(t,n,i){n[i]=t>>>24,n[i+1]=t>>>16&255,n[i+2]=t>>>8&255,n[i+3]=255&t}function p(t,n){return(t[n]|t[n+1]<<8|t[n+2]<<16|t[n+3]<<24)>>>0}function m(t,n){return(t[n]<<24|t[n+1]<<16|t[n+2]<<8|t[n+3])>>>0}n.exports=r(r)},{}],5:[function(t,n,i){function r(t){try{var n=eval("require")(t);if(n&&(n.length||Object.keys(n).length))return n}catch(t){}return null}n.exports=r},{}],6:[function(t,n,i){n.exports=function(n,i,t){var r=t||8192,u=r>>>1,e=null,s=r;return function(t){if(t<1||u>10),e[s++]=56320+(1023&r)):e[s++]=(15&r)<<12|(63&t[n++])<<6|63&t[n++],8191>6|192:(55296==(64512&r)&&56320==(64512&(u=t.charCodeAt(s+1)))?(++s,n[i++]=(r=65536+((1023&r)<<10)+(1023&u))>>18|240,n[i++]=r>>12&63|128):n[i++]=r>>12|224,n[i++]=r>>6&63|128),n[i++]=63&r|128);return i-e}},{}],8:[function(t,n,i){var r=i;function u(){r.util.n(),r.Writer.n(r.BufferWriter),r.Reader.n(r.BufferReader)}r.build="minimal",r.Writer=t(16),r.BufferWriter=t(17),r.Reader=t(9),r.BufferReader=t(10),r.util=t(15),r.rpc=t(12),r.roots=t(11),r.configure=u,u()},{10:10,11:11,12:12,15:15,16:16,17:17,9:9}],9:[function(t,n,i){n.exports=o;var r,u=t(15),e=u.LongBits,s=u.utf8;function h(t,n){return RangeError("index out of range: "+t.pos+" + "+(n||1)+" > "+t.len)}function o(t){this.buf=t,this.pos=0,this.len=t.length}function f(){return u.Buffer?function(t){return(o.create=function(t){return u.Buffer.isBuffer(t)?new r(t):a(t)})(t)}:a}var c,a="undefined"!=typeof Uint8Array?function(t){if(t instanceof Uint8Array||Array.isArray(t))return new o(t);throw Error("illegal buffer")}:function(t){if(Array.isArray(t))return new o(t);throw Error("illegal buffer")};function l(){var t=new e(0,0),n=0;if(!(4=this.len)throw h(this);if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*n)>>>0,this.buf[this.pos++]<128)return t}return t.lo=(t.lo|(127&this.buf[this.pos++])<<7*n)>>>0,t}for(;n<4;++n)if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*n)>>>0,this.buf[this.pos++]<128)return t;if(t.lo=(t.lo|(127&this.buf[this.pos])<<28)>>>0,t.hi=(t.hi|(127&this.buf[this.pos])>>4)>>>0,this.buf[this.pos++]<128)return t;if(n=0,4>>0,this.buf[this.pos++]<128)return t}else for(;n<5;++n){if(this.pos>=this.len)throw h(this);if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*n+3)>>>0,this.buf[this.pos++]<128)return t}throw Error("invalid varint encoding")}function v(t,n){return(t[n-4]|t[n-3]<<8|t[n-2]<<16|t[n-1]<<24)>>>0}function w(){if(this.pos+8>this.len)throw h(this,8);return new e(v(this.buf,this.pos+=4),v(this.buf,this.pos+=4))}o.create=f(),o.prototype.i=u.Array.prototype.subarray||u.Array.prototype.slice,o.prototype.uint32=(c=4294967295,function(){if(c=(127&this.buf[this.pos])>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<7)>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<14)>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<21)>>>0,this.buf[this.pos++]<128||(c=(c|(15&this.buf[this.pos])<<28)>>>0,this.buf[this.pos++]<128||!((this.pos+=5)>this.len))))))return c;throw this.pos=this.len,h(this,10)}),o.prototype.int32=function(){return 0|this.uint32()},o.prototype.sint32=function(){var t=this.uint32();return t>>>1^-(1&t)|0},o.prototype.bool=function(){return 0!==this.uint32()},o.prototype.fixed32=function(){if(this.pos+4>this.len)throw h(this,4);return v(this.buf,this.pos+=4)},o.prototype.sfixed32=function(){if(this.pos+4>this.len)throw h(this,4);return 0|v(this.buf,this.pos+=4)},o.prototype.float=function(){if(this.pos+4>this.len)throw h(this,4);var t=u.float.readFloatLE(this.buf,this.pos);return this.pos+=4,t},o.prototype.double=function(){if(this.pos+8>this.len)throw h(this,4);var t=u.float.readDoubleLE(this.buf,this.pos);return this.pos+=8,t},o.prototype.bytes=function(){var t=this.uint32(),n=this.pos,i=this.pos+t;if(i>this.len)throw h(this,t);return this.pos+=t,Array.isArray(this.buf)?this.buf.slice(n,i):n===i?(t=u.Buffer)?t.alloc(0):new this.buf.constructor(0):this.i.call(this.buf,n,i)},o.prototype.string=function(){var t=this.bytes();return s.read(t,0,t.length)},o.prototype.skip=function(t){if("number"==typeof t){if(this.pos+t>this.len)throw h(this,t);this.pos+=t}else do{if(this.pos>=this.len)throw h(this)}while(128&this.buf[this.pos++]);return this},o.prototype.skipType=function(t){switch(t){case 0:this.skip();break;case 1:this.skip(8);break;case 2:this.skip(this.uint32());break;case 3:for(;4!=(t=7&this.uint32());)this.skipType(t);break;case 5:this.skip(4);break;default:throw Error("invalid wire type "+t+" at offset "+this.pos)}return this},o.n=function(t){r=t,o.create=f(),r.n();var n=u.Long?"toLong":"toNumber";u.merge(o.prototype,{int64:function(){return l.call(this)[n](!1)},uint64:function(){return l.call(this)[n](!0)},sint64:function(){return l.call(this).zzDecode()[n](!1)},fixed64:function(){return w.call(this)[n](!0)},sfixed64:function(){return w.call(this)[n](!1)}})}},{15:15}],10:[function(t,n,i){n.exports=e;var r=t(9),u=((e.prototype=Object.create(r.prototype)).constructor=e,t(15));function e(t){r.call(this,t)}e.n=function(){u.Buffer&&(e.prototype.i=u.Buffer.prototype.slice)},e.prototype.string=function(){var t=this.uint32();return this.buf.utf8Slice?this.buf.utf8Slice(this.pos,this.pos=Math.min(this.pos+t,this.len)):this.buf.toString("utf-8",this.pos,this.pos=Math.min(this.pos+t,this.len))},e.n()},{15:15,9:9}],11:[function(t,n,i){n.exports={}},{}],12:[function(t,n,i){i.Service=t(13)},{13:13}],13:[function(t,n,i){n.exports=r;var h=t(15);function r(t,n,i){if("function"!=typeof t)throw TypeError("rpcImpl must be a function");h.EventEmitter.call(this),this.rpcImpl=t,this.requestDelimited=!!n,this.responseDelimited=!!i}((r.prototype=Object.create(h.EventEmitter.prototype)).constructor=r).prototype.rpcCall=function t(i,n,r,u,e){if(!u)throw TypeError("request must be specified");var s=this;if(!e)return h.asPromise(t,s,i,n,r,u);if(!s.rpcImpl)return setTimeout(function(){e(Error("already ended"))},0),d;try{return s.rpcImpl(i,n[s.requestDelimited?"encodeDelimited":"encode"](u).finish(),function(t,n){if(t)return s.emit("error",t,i),e(t);if(null===n)return s.end(!0),d;if(!(n instanceof r))try{n=r[s.responseDelimited?"decodeDelimited":"decode"](n)}catch(t){return s.emit("error",t,i),e(t)}return s.emit("data",n,i),e(null,n)})}catch(t){return s.emit("error",t,i),setTimeout(function(){e(t)},0),d}},r.prototype.end=function(t){return this.rpcImpl&&(t||this.rpcImpl(null,null,null),this.rpcImpl=null,this.emit("end").off()),this}},{15:15}],14:[function(t,n,i){n.exports=u;var r=t(15);function u(t,n){this.lo=t>>>0,this.hi=n>>>0}var e=u.zero=new u(0,0),s=(e.toNumber=function(){return 0},e.zzEncode=e.zzDecode=function(){return this},e.length=function(){return 1},u.zeroHash="\0\0\0\0\0\0\0\0",u.fromNumber=function(t){var n,i;return 0===t?e:(i=(t=(n=t<0)?-t:t)>>>0,t=(t-i)/4294967296>>>0,n&&(t=~t>>>0,i=~i>>>0,4294967295<++i&&(i=0,4294967295<++t&&(t=0))),new u(i,t))},u.from=function(t){if("number"==typeof t)return u.fromNumber(t);if(r.isString(t)){if(!r.Long)return u.fromNumber(parseInt(t,10));t=r.Long.fromString(t)}return t.low||t.high?new u(t.low>>>0,t.high>>>0):e},u.prototype.toNumber=function(t){var n;return!t&&this.hi>>>31?(t=1+~this.lo>>>0,n=~this.hi>>>0,-(t+4294967296*(n=t?n:n+1>>>0))):this.lo+4294967296*this.hi},u.prototype.toLong=function(t){return r.Long?new r.Long(0|this.lo,0|this.hi,!!t):{low:0|this.lo,high:0|this.hi,unsigned:!!t}},String.prototype.charCodeAt);u.fromHash=function(t){return"\0\0\0\0\0\0\0\0"===t?e:new u((s.call(t,0)|s.call(t,1)<<8|s.call(t,2)<<16|s.call(t,3)<<24)>>>0,(s.call(t,4)|s.call(t,5)<<8|s.call(t,6)<<16|s.call(t,7)<<24)>>>0)},u.prototype.toHash=function(){return String.fromCharCode(255&this.lo,this.lo>>>8&255,this.lo>>>16&255,this.lo>>>24,255&this.hi,this.hi>>>8&255,this.hi>>>16&255,this.hi>>>24)},u.prototype.zzEncode=function(){var t=this.hi>>31;return this.hi=((this.hi<<1|this.lo>>>31)^t)>>>0,this.lo=(this.lo<<1^t)>>>0,this},u.prototype.zzDecode=function(){var t=-(1&this.lo);return this.lo=((this.lo>>>1|this.hi<<31)^t)>>>0,this.hi=(this.hi>>>1^t)>>>0,this},u.prototype.length=function(){var t=this.lo,n=(this.lo>>>28|this.hi<<4)>>>0,i=this.hi>>>24;return 0==i?0==n?t<16384?t<128?1:2:t<2097152?3:4:n<16384?n<128?5:6:n<2097152?7:8:i<128?9:10}},{15:15}],15:[function(t,n,i){var r=i;function u(t,n,i){for(var r=Object.keys(n),u=0;u>>7|t.hi<<25)>>>0,t.hi>>>=7;for(;127>>7;n[i++]=t.lo}function y(t,n,i){n[i]=255&t,n[i+1]=t>>>8&255,n[i+2]=t>>>16&255,n[i+3]=t>>>24}a.create=l(),a.alloc=function(t){return new u.Array(t)},u.Array!==Array&&(a.alloc=u.pool(a.alloc,u.Array.prototype.subarray)),a.prototype.e=function(t,n,i){return this.tail=this.tail.next=new o(t,n,i),this.len+=n,this},(w.prototype=Object.create(o.prototype)).fn=function(t,n,i){for(;127>>=7;n[i]=t},a.prototype.uint32=function(t){return this.len+=(this.tail=this.tail.next=new w((t>>>=0)<128?1:t<16384?2:t<2097152?3:t<268435456?4:5,t)).len,this},a.prototype.int32=function(t){return t<0?this.e(b,10,e.fromNumber(t)):this.uint32(t)},a.prototype.sint32=function(t){return this.uint32((t<<1^t>>31)>>>0)},a.prototype.int64=a.prototype.uint64=function(t){t=e.from(t);return this.e(b,t.length(),t)},a.prototype.sint64=function(t){t=e.from(t).zzEncode();return this.e(b,t.length(),t)},a.prototype.bool=function(t){return this.e(v,1,t?1:0)},a.prototype.sfixed32=a.prototype.fixed32=function(t){return this.e(y,4,t>>>0)},a.prototype.sfixed64=a.prototype.fixed64=function(t){t=e.from(t);return this.e(y,4,t.lo).e(y,4,t.hi)},a.prototype.float=function(t){return this.e(u.float.writeFloatLE,4,t)},a.prototype.double=function(t){return this.e(u.float.writeDoubleLE,8,t)};var g=u.Array.prototype.set?function(t,n,i){n.set(t,i)}:function(t,n,i){for(var r=0;r>>0;return i?(u.isString(t)&&(n=a.alloc(i=s.length(t)),s.decode(t,n,0),t=n),this.uint32(i).e(g,i,t)):this.e(v,1,0)},a.prototype.string=function(t){var n=h.length(t);return n?this.uint32(n).e(h.write,n,t):this.e(v,1,0)},a.prototype.fork=function(){return this.states=new c(this),this.head=this.tail=new o(f,0,0),this.len=0,this},a.prototype.reset=function(){return this.states?(this.head=this.states.head,this.tail=this.states.tail,this.len=this.states.len,this.states=this.states.next):(this.head=this.tail=new o(f,0,0),this.len=0),this},a.prototype.ldelim=function(){var t=this.head,n=this.tail,i=this.len;return this.reset().uint32(i),i&&(this.tail.next=t.next,this.tail=n,this.len+=i),this},a.prototype.finish=function(){for(var t=this.head.next,n=this.constructor.alloc(this.len),i=0;t;)t.fn(t.val,n,i),i+=t.len,t=t.next;return n},a.n=function(t){r=t,a.create=l(),r.n()}},{15:15}],17:[function(t,n,i){n.exports=e;var r=t(16),u=((e.prototype=Object.create(r.prototype)).constructor=e,t(15));function e(){r.call(this)}function s(t,n,i){t.length<40?u.utf8.write(t,n,i):n.utf8Write?n.utf8Write(t,i):n.write(t,i)}e.n=function(){e.alloc=u.u,e.writeBytesBuffer=u.Buffer&&u.Buffer.prototype instanceof Uint8Array&&"set"===u.Buffer.prototype.set.name?function(t,n,i){n.set(t,i)}:function(t,n,i){if(t.copy)t.copy(n,i,0,t.length);else for(var r=0;r>>0;return this.uint32(n),n&&this.e(e.writeBytesBuffer,n,t),this},e.prototype.string=function(t){var n=u.Buffer.byteLength(t);return this.uint32(n),n&&this.e(s,n,t),this},e.n()},{15:15,16:16}]},{},[8])}(); -+!function(d){"use strict";!function(r,u,t){var n=function t(n){var i=u[n];return i||r[n][0].call(i=u[n]={exports:{}},t,i,i.exports),i.exports}(t[0]);n.util.global.protobuf=n,"function"==typeof define&&define.amd&&define(["long"],function(t){return t&&t.isLong&&(n.util.Long=t,n.configure()),n}),"object"==typeof module&&module&&module.exports&&(module.exports=n)}({1:[function(t,n,i){n.exports=function(t,n){var i=Array(arguments.length-1),e=0,r=2,s=!0;for(;r>2],r=(3&o)<<4,h=1;break;case 1:e[s++]=f[r|o>>4],r=(15&o)<<2,h=2;break;case 2:e[s++]=f[r|o>>6],e[s++]=f[63&o],h=0}8191>4,r=h,e=2;break;case 2:n[i++]=(15&r)<<4|(60&h)>>2,r=h,e=3;break;case 3:n[i++]=(3&r)<<6|h,e=0}}if(1===e)throw Error(c);return i-u},i.test=function(t){return/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(t)}},{}],3:[function(t,n,i){function r(){this.t={}}(n.exports=r).prototype.on=function(t,n,i){return(this.t[t]||(this.t[t]=[])).push({fn:n,ctx:i||this}),this},r.prototype.off=function(t,n){if(t===d)this.t={};else if(n===d)this.t[t]=[];else for(var i=this.t[t],r=0;r>>0:n<11754943508222875e-54?(u<<31|Math.round(n/1401298464324817e-60))>>>0:(u<<31|127+(t=Math.floor(Math.log(n)/Math.LN2))<<23|8388607&Math.round(n*Math.pow(2,-t)*8388608))>>>0,i,r)}function i(t,n,i){t=t(n,i),n=2*(t>>31)+1,i=t>>>23&255,t&=8388607;return 255==i?t?NaN:1/0*n:0==i?1401298464324817e-60*n*t:n*Math.pow(2,i-150)*(8388608+t)}function r(t,n,i){h[0]=t,n[i]=o[0],n[i+1]=o[1],n[i+2]=o[2],n[i+3]=o[3]}function u(t,n,i){h[0]=t,n[i]=o[3],n[i+1]=o[2],n[i+2]=o[1],n[i+3]=o[0]}function e(t,n){return o[0]=t[n],o[1]=t[n+1],o[2]=t[n+2],o[3]=t[n+3],h[0]}function s(t,n){return o[3]=t[n],o[2]=t[n+1],o[1]=t[n+2],o[0]=t[n+3],h[0]}var h,o,f,c,a;function l(t,n,i,r,u,e){var s,h=r<0?1:0;0===(r=h?-r:r)?(t(0,u,e+n),t(0<1/r?0:2147483648,u,e+i)):isNaN(r)?(t(0,u,e+n),t(2146959360,u,e+i)):17976931348623157e292>>0,u,e+i)):r<22250738585072014e-324?(t((s=r/5e-324)>>>0,u,e+n),t((h<<31|s/4294967296)>>>0,u,e+i)):(t(4503599627370496*(s=r*Math.pow(2,-(r=1024===(r=Math.floor(Math.log(r)/Math.LN2))?1023:r)))>>>0,u,e+n),t((h<<31|r+1023<<20|1048576*s&1048575)>>>0,u,e+i))}function v(t,n,i,r,u){n=t(r,u+n),t=t(r,u+i),r=2*(t>>31)+1,u=t>>>20&2047,i=4294967296*(1048575&t)+n;return 2047==u?i?NaN:1/0*r:0==u?5e-324*r*i:r*Math.pow(2,u-1075)*(i+4503599627370496)}function w(t,n,i){f[0]=t,n[i]=c[0],n[i+1]=c[1],n[i+2]=c[2],n[i+3]=c[3],n[i+4]=c[4],n[i+5]=c[5],n[i+6]=c[6],n[i+7]=c[7]}function b(t,n,i){f[0]=t,n[i]=c[7],n[i+1]=c[6],n[i+2]=c[5],n[i+3]=c[4],n[i+4]=c[3],n[i+5]=c[2],n[i+6]=c[1],n[i+7]=c[0]}function y(t,n){return c[0]=t[n],c[1]=t[n+1],c[2]=t[n+2],c[3]=t[n+3],c[4]=t[n+4],c[5]=t[n+5],c[6]=t[n+6],c[7]=t[n+7],f[0]}function g(t,n){return c[7]=t[n],c[6]=t[n+1],c[5]=t[n+2],c[4]=t[n+3],c[3]=t[n+4],c[2]=t[n+5],c[1]=t[n+6],c[0]=t[n+7],f[0]}return"undefined"!=typeof Float32Array?(h=new Float32Array([-0]),o=new Uint8Array(h.buffer),a=128===o[3],t.writeFloatLE=a?r:u,t.writeFloatBE=a?u:r,t.readFloatLE=a?e:s,t.readFloatBE=a?s:e):(t.writeFloatLE=n.bind(null,d),t.writeFloatBE=n.bind(null,A),t.readFloatLE=i.bind(null,p),t.readFloatBE=i.bind(null,m)),"undefined"!=typeof Float64Array?(f=new Float64Array([-0]),c=new Uint8Array(f.buffer),a=128===c[7],t.writeDoubleLE=a?w:b,t.writeDoubleBE=a?b:w,t.readDoubleLE=a?y:g,t.readDoubleBE=a?g:y):(t.writeDoubleLE=l.bind(null,d,0,4),t.writeDoubleBE=l.bind(null,A,4,0),t.readDoubleLE=v.bind(null,p,0,4),t.readDoubleBE=v.bind(null,m,4,0)),t}function d(t,n,i){n[i]=255&t,n[i+1]=t>>>8&255,n[i+2]=t>>>16&255,n[i+3]=t>>>24}function A(t,n,i){n[i]=t>>>24,n[i+1]=t>>>16&255,n[i+2]=t>>>8&255,n[i+3]=255&t}function p(t,n){return(t[n]|t[n+1]<<8|t[n+2]<<16|t[n+3]<<24)>>>0}function m(t,n){return(t[n]<<24|t[n+1]<<16|t[n+2]<<8|t[n+3])>>>0}n.exports=r(r)},{}],5:[function(t,n,i){function r(t){try{var n=eval("require")(t);if(n&&(n.length||Object.keys(n).length))return n}catch(t){}return null}n.exports=r},{}],6:[function(t,n,i){n.exports=function(n,i,t){var r=t||8192,u=r>>>1,e=null,s=r;return function(t){if(t<1||u>10),e[s++]=56320+(1023&r)):e[s++]=(15&r)<<12|(63&t[n++])<<6|63&t[n++],8191>6|192:(55296==(64512&r)&&56320==(64512&(u=t.charCodeAt(s+1)))?(++s,n[i++]=(r=65536+((1023&r)<<10)+(1023&u))>>18|240,n[i++]=r>>12&63|128):n[i++]=r>>12|224,n[i++]=r>>6&63|128),n[i++]=63&r|128);return i-e}},{}],8:[function(t,n,i){var r=i;function u(){r.util.n(),r.Writer.n(r.BufferWriter),r.Reader.n(r.BufferReader)}r.build="minimal",r.Writer=t(16),r.BufferWriter=t(17),r.Reader=t(9),r.BufferReader=t(10),r.util=t(15),r.rpc=t(12),r.roots=t(11),r.configure=u,u()},{10:10,11:11,12:12,15:15,16:16,17:17,9:9}],9:[function(t,n,i){n.exports=o;var r,u=t(15),e=u.LongBits,s=u.utf8;function h(t,n){return RangeError("index out of range: "+t.pos+" + "+(n||1)+" > "+t.len)}function o(t){this.buf=t,this.pos=0,this.len=t.length}function f(){return u.Buffer?function(t){return(o.create=function(t){return u.Buffer.isBuffer(t)?new r(t):a(t)})(t)}:a}var c,a="undefined"!=typeof Uint8Array?function(t){if(t instanceof Uint8Array||Array.isArray(t))return new o(t);throw Error("illegal buffer")}:function(t){if(Array.isArray(t))return new o(t);throw Error("illegal buffer")};function l(){var t=new e(0,0),n=0;if(!(4=this.len)throw h(this);if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*n)>>>0,this.buf[this.pos++]<128)return t}return t.lo=(t.lo|(127&this.buf[this.pos++])<<7*n)>>>0,t}for(;n<4;++n)if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*n)>>>0,this.buf[this.pos++]<128)return t;if(t.lo=(t.lo|(127&this.buf[this.pos])<<28)>>>0,t.hi=(t.hi|(127&this.buf[this.pos])>>4)>>>0,this.buf[this.pos++]<128)return t;if(n=0,4>>0,this.buf[this.pos++]<128)return t}else for(;n<5;++n){if(this.pos>=this.len)throw h(this);if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*n+3)>>>0,this.buf[this.pos++]<128)return t}throw Error("invalid varint encoding")}function v(t,n){return(t[n-4]|t[n-3]<<8|t[n-2]<<16|t[n-1]<<24)>>>0}function w(){if(this.pos+8>this.len)throw h(this,8);return new e(v(this.buf,this.pos+=4),v(this.buf,this.pos+=4))}o.create=f(),o.prototype.i=u.Array.prototype.subarray||u.Array.prototype.slice,o.prototype.uint32=(c=4294967295,function(){if(c=(127&this.buf[this.pos])>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<7)>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<14)>>>0,this.buf[this.pos++]<128||(c=(c|(127&this.buf[this.pos])<<21)>>>0,this.buf[this.pos++]<128||(c=(c|(15&this.buf[this.pos])<<28)>>>0,this.buf[this.pos++]<128||!((this.pos+=5)>this.len))))))return c;throw this.pos=this.len,h(this,10)}),o.prototype.int32=function(){return 0|this.uint32()},o.prototype.sint32=function(){var t=this.uint32();return t>>>1^-(1&t)|0},o.prototype.bool=function(){return 0!==this.uint32()},o.prototype.fixed32=function(){if(this.pos+4>this.len)throw h(this,4);return v(this.buf,this.pos+=4)},o.prototype.sfixed32=function(){if(this.pos+4>this.len)throw h(this,4);return 0|v(this.buf,this.pos+=4)},o.prototype.float=function(){if(this.pos+4>this.len)throw h(this,4);var t=u.float.readFloatLE(this.buf,this.pos);return this.pos+=4,t},o.prototype.double=function(){if(this.pos+8>this.len)throw h(this,4);var t=u.float.readDoubleLE(this.buf,this.pos);return this.pos+=8,t},o.prototype.bytes=function(){var t=this.uint32(),n=this.pos,i=this.pos+t;if(i>this.len)throw h(this,t);return this.pos+=t,Array.isArray(this.buf)?this.buf.slice(n,i):n===i?(t=u.Buffer)?t.alloc(0):new this.buf.constructor(0):this.i.call(this.buf,n,i)},o.prototype.string=function(){var t=this.bytes();return s.read(t,0,t.length)},o.prototype.skip=function(t){if("number"==typeof t){if(this.pos+t>this.len)throw h(this,t);this.pos+=t}else do{if(this.pos>=this.len)throw h(this)}while(128&this.buf[this.pos++]);return this},o.prototype.skipType=function(t){switch(t){case 0:this.skip();break;case 1:this.skip(8);break;case 2:this.skip(this.uint32());break;case 3:for(;4!=(t=7&this.uint32());)this.skipType(t);break;case 5:this.skip(4);break;default:throw Error("invalid wire type "+t+" at offset "+this.pos)}return this},o.n=function(t){r=t,o.create=f(),r.n();var n=u.Long?"toLong":"toNumber";u.merge(o.prototype,{int64:function(){return l.call(this)[n](!1)},uint64:function(){return l.call(this)[n](!0)},sint64:function(){return l.call(this).zzDecode()[n](!1)},fixed64:function(){return w.call(this)[n](!0)},sfixed64:function(){return w.call(this)[n](!1)}})}},{15:15}],10:[function(t,n,i){n.exports=e;var r=t(9),u=((e.prototype=Object.create(r.prototype)).constructor=e,t(15));function e(t){r.call(this,t)}e.n=function(){u.Buffer&&(e.prototype.i=u.Buffer.prototype.slice)},e.prototype.string=function(){var t=this.uint32();return this.buf.utf8Slice?this.buf.utf8Slice(this.pos,this.pos=Math.min(this.pos+t,this.len)):this.buf.toString("utf-8",this.pos,this.pos=Math.min(this.pos+t,this.len))},e.n()},{15:15,9:9}],11:[function(t,n,i){n.exports={}},{}],12:[function(t,n,i){i.Service=t(13)},{13:13}],13:[function(t,n,i){n.exports=r;var h=t(15);function r(t,n,i){if("function"!=typeof t)throw TypeError("rpcImpl must be a function");h.EventEmitter.call(this),this.rpcImpl=t,this.requestDelimited=!!n,this.responseDelimited=!!i}((r.prototype=Object.create(h.EventEmitter.prototype)).constructor=r).prototype.rpcCall=function t(i,n,r,u,e){if(!u)throw TypeError("request must be specified");var s=this;if(!e)return h.asPromise(t,s,i,n,r,u);if(!s.rpcImpl)return setTimeout(function(){e(Error("already ended"))},0),d;try{return s.rpcImpl(i,n[s.requestDelimited?"encodeDelimited":"encode"](u).finish(),function(t,n){if(t)return s.emit("error",t,i),e(t);if(null===n)return s.end(!0),d;if(!(n instanceof r))try{n=r[s.responseDelimited?"decodeDelimited":"decode"](n)}catch(t){return s.emit("error",t,i),e(t)}return s.emit("data",n,i),e(null,n)})}catch(t){return s.emit("error",t,i),setTimeout(function(){e(t)},0),d}},r.prototype.end=function(t){return this.rpcImpl&&(t||this.rpcImpl(null,null,null),this.rpcImpl=null,this.emit("end").off()),this}},{15:15}],14:[function(t,n,i){n.exports=u;var r=t(15);function u(t,n){this.lo=t>>>0,this.hi=n>>>0}var e=u.zero=new u(0,0),s=(e.toNumber=function(){return 0},e.zzEncode=e.zzDecode=function(){return this},e.length=function(){return 1},u.zeroHash="\0\0\0\0\0\0\0\0",u.fromNumber=function(t){var n,i;return 0===t?e:(i=(t=(n=t<0)?-t:t)>>>0,t=(t-i)/4294967296>>>0,n&&(t=~t>>>0,i=~i>>>0,4294967295<++i&&(i=0,4294967295<++t&&(t=0))),new u(i,t))},u.from=function(t){if("number"==typeof t)return u.fromNumber(t);if(r.isString(t)){if(!r.Long)return u.fromNumber(parseInt(t,10));t=r.Long.fromString(t)}return t.low||t.high?new u(t.low>>>0,t.high>>>0):e},u.prototype.toNumber=function(t){var n;return!t&&this.hi>>>31?(t=1+~this.lo>>>0,n=~this.hi>>>0,-(t+4294967296*(n=t?n:n+1>>>0))):this.lo+4294967296*this.hi},u.prototype.toLong=function(t){return r.Long?new r.Long(0|this.lo,0|this.hi,!!t):{low:0|this.lo,high:0|this.hi,unsigned:!!t}},String.prototype.charCodeAt);u.fromHash=function(t){return"\0\0\0\0\0\0\0\0"===t?e:new u((s.call(t,0)|s.call(t,1)<<8|s.call(t,2)<<16|s.call(t,3)<<24)>>>0,(s.call(t,4)|s.call(t,5)<<8|s.call(t,6)<<16|s.call(t,7)<<24)>>>0)},u.prototype.toHash=function(){return String.fromCharCode(255&this.lo,this.lo>>>8&255,this.lo>>>16&255,this.lo>>>24,255&this.hi,this.hi>>>8&255,this.hi>>>16&255,this.hi>>>24)},u.prototype.zzEncode=function(){var t=this.hi>>31;return this.hi=((this.hi<<1|this.lo>>>31)^t)>>>0,this.lo=(this.lo<<1^t)>>>0,this},u.prototype.zzDecode=function(){var t=-(1&this.lo);return this.lo=((this.lo>>>1|this.hi<<31)^t)>>>0,this.hi=(this.hi>>>1^t)>>>0,this},u.prototype.length=function(){var t=this.lo,n=(this.lo>>>28|this.hi<<4)>>>0,i=this.hi>>>24;return 0==i?0==n?t<16384?t<128?1:2:t<2097152?3:4:n<16384?n<128?5:6:n<2097152?7:8:i<128?9:10}},{15:15}],15:[function(t,n,i){var r=i;function u(t,n,i){for(var r=Object.keys(n),u=0;u>>7|t.hi<<25)>>>0,t.hi>>>=7;for(;127>>7;n[i++]=t.lo}function y(t,n,i){n[i]=255&t,n[i+1]=t>>>8&255,n[i+2]=t>>>16&255,n[i+3]=t>>>24}a.create=l(),a.alloc=function(t){return new u.Array(t)},u.Array!==Array&&(a.alloc=u.pool(a.alloc,u.Array.prototype.subarray)),a.prototype.e=function(t,n,i){return this.tail=this.tail.next=new o(t,n,i),this.len+=n,this},(w.prototype=Object.create(o.prototype)).fn=function(t,n,i){for(;127>>=7;n[i]=t},a.prototype.uint32=function(t){return this.len+=(this.tail=this.tail.next=new w((t>>>=0)<128?1:t<16384?2:t<2097152?3:t<268435456?4:5,t)).len,this},a.prototype.int32=function(t){return t<0?this.e(b,10,e.fromNumber(t)):this.uint32(t)},a.prototype.sint32=function(t){return this.uint32((t<<1^t>>31)>>>0)},a.prototype.int64=a.prototype.uint64=function(t){t=e.from(t);return this.e(b,t.length(),t)},a.prototype.sint64=function(t){t=e.from(t).zzEncode();return this.e(b,t.length(),t)},a.prototype.bool=function(t){return this.e(v,1,t?1:0)},a.prototype.sfixed32=a.prototype.fixed32=function(t){return this.e(y,4,t>>>0)},a.prototype.sfixed64=a.prototype.fixed64=function(t){t=e.from(t);return this.e(y,4,t.lo).e(y,4,t.hi)},a.prototype.float=function(t){return this.e(u.float.writeFloatLE,4,t)},a.prototype.double=function(t){return this.e(u.float.writeDoubleLE,8,t)};var g=u.Array.prototype.set?function(t,n,i){n.set(t,i)}:function(t,n,i){for(var r=0;r>>0;return i?(u.isString(t)&&(n=a.alloc(i=s.length(t)),s.decode(t,n,0),t=n),this.uint32(i).e(g,i,t)):this.e(v,1,0)},a.prototype.s=function(t){return this.e(g,t.length,t)},a.prototype.string=function(t){var n=h.length(t);return n?this.uint32(n).e(h.write,n,t):this.e(v,1,0)},a.prototype.fork=function(){return this.states=new c(this),this.head=this.tail=new o(f,0,0),this.len=0,this},a.prototype.reset=function(){return this.states?(this.head=this.states.head,this.tail=this.states.tail,this.len=this.states.len,this.states=this.states.next):(this.head=this.tail=new o(f,0,0),this.len=0),this},a.prototype.ldelim=function(){var t=this.head,n=this.tail,i=this.len;return this.reset().uint32(i),i&&(this.tail.next=t.next,this.tail=n,this.len+=i),this},a.prototype.finish=function(){for(var t=this.head.next,n=this.constructor.alloc(this.len),i=0;t;)t.fn(t.val,n,i),i+=t.len,t=t.next;return n},a.n=function(t){r=t,a.create=l(),r.n()}},{15:15}],17:[function(t,n,i){n.exports=e;var r=t(16),u=((e.prototype=Object.create(r.prototype)).constructor=e,t(15));function e(){r.call(this)}function s(t,n,i){t.length<40?u.utf8.write(t,n,i):n.utf8Write?n.utf8Write(t,i):n.write(t,i)}e.n=function(){e.alloc=u.u,e.writeBytesBuffer=u.Buffer&&u.Buffer.prototype instanceof Uint8Array&&"set"===u.Buffer.prototype.set.name?function(t,n,i){n.set(t,i)}:function(t,n,i){if(t.copy)t.copy(n,i,0,t.length);else for(var r=0;r>>0;return this.uint32(n),n&&this.e(e.writeBytesBuffer,n,t),this},e.prototype.string=function(t){var n=u.Buffer.byteLength(t);return this.uint32(n),n&&this.e(s,n,t),this},e.n()},{15:15,16:16}]},{},[8])}(); - //# sourceMappingURL=protobuf.min.js.map -diff --git a/dist/protobuf.js b/dist/protobuf.js -index 067305a..8f36174 100644 ---- a/dist/protobuf.js -+++ b/dist/protobuf.js -@@ -1,6 +1,6 @@ - /*! - * protobuf.js v7.3.2 (c) 2016, daniel wirtz -- * compiled wed, 12 jun 2024 08:24:21 utc -+ * compiled mon, 24 jun 2024 23:18:03 utc - * licensed under the bsd-3-clause license - * see: https://github.com/dcodeio/protobuf.js for details - */ -@@ -1836,6 +1836,7 @@ function decoder(mtype) { - ("r=Reader.create(r)") - ("var c=l===undefined?r.len:r.pos+l,m=new this.ctor" + (mtype.fieldsArray.filter(function(field) { return field.map; }).length ? ",k,value" : "")) - ("while(r.pos>>3){"); - -+ var unknownRef = "m" + util.safeProp("$unknownFields"); -+ - var i = 0; - for (; i < /* initializes */ mtype.fieldsArray.length; ++i) { - var field = mtype._fieldsArray[i].resolve(), -@@ -1925,6 +1928,11 @@ function decoder(mtype) { - } gen - ("default:") - ("r.skipType(t&7)") -+ ("if (!(%s)) {", unknownRef) -+ ("%s = []", unknownRef) -+ ("}") -+ -+ ("%s.push(r.buf.slice(unknownStartPos, r.pos))", unknownRef) - ("break") - - ("}") -@@ -1982,6 +1990,21 @@ function encoder(mtype) { - // "when a message is serialized its known fields should be written sequentially by field number" - var fields = /* initializes */ mtype.fieldsArray.slice().sort(util.compareFieldsById); - -+ var unknownRef = "m" + util.safeProp("$unknownFields"); -+ -+ // Redecode unknown fields and apply them to the message before encoding -+ gen -+ ("var fullyUnknown=[]") -+ ("if(%s&&this.ctor.decode) {", unknownRef) -+ ("for(var i=0;i<%s.length;++i) {", unknownRef) -+ ("try {") -+ ("var known=this.ctor.decode(%s[i])", unknownRef) -+ ("fullyUnknown=fullyUnknown.concat(known.$unknownFields||[])") -+ ("m=Object.assign(known,m)") -+ ("}catch(_){}") -+ ("}") -+ ("}"); -+ - for (var i = 0; i < fields.length; ++i) { - var field = fields[i].resolve(), - index = mtype._fieldsArray.indexOf(field), -@@ -2040,6 +2063,11 @@ function encoder(mtype) { - } - } - -+ gen -+ ("for(var i=0;i>2],r=(3&f)<<4,u=1;break;case 1:s[o++]=h[r|f>>4],r=(15&f)<<2,u=2;break;case 2:s[o++]=h[r|f>>6],s[o++]=h[63&f],u=0}8191>4,r=u,s=2;break;case 2:i[n++]=(15&r)<<4|(60&u)>>2,r=u,s=3;break;case 3:i[n++]=(3&r)<<6|u,s=0}}if(1===s)throw Error(a);return n-e},n.test=function(t){return/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(t)}},{}],3:[function(t,i,n){function c(i,n){"string"==typeof i&&(n=i,i=nt);var f=[];function h(t){if("string"!=typeof t){var i=a();if(c.verbose&&console.log("codegen: "+i),i="return "+i,t){for(var n=Object.keys(t),r=Array(n.length+1),e=Array(n.length),s=0;s>>0:i<11754943508222875e-54?(e<<31|Math.round(i/1401298464324817e-60))>>>0:(e<<31|127+(t=Math.floor(Math.log(i)/Math.LN2))<<23|8388607&Math.round(i*Math.pow(2,-t)*8388608))>>>0,n,r)}function n(t,i,n){t=t(i,n),i=2*(t>>31)+1,n=t>>>23&255,t&=8388607;return 255==n?t?NaN:1/0*i:0==n?1401298464324817e-60*i*t:i*Math.pow(2,n-150)*(8388608+t)}function r(t,i,n){u[0]=t,i[n]=f[0],i[n+1]=f[1],i[n+2]=f[2],i[n+3]=f[3]}function e(t,i,n){u[0]=t,i[n]=f[3],i[n+1]=f[2],i[n+2]=f[1],i[n+3]=f[0]}function s(t,i){return f[0]=t[i],f[1]=t[i+1],f[2]=t[i+2],f[3]=t[i+3],u[0]}function o(t,i){return f[3]=t[i],f[2]=t[i+1],f[1]=t[i+2],f[0]=t[i+3],u[0]}var u,f,h,a,c;function l(t,i,n,r,e,s){var o,u=r<0?1:0;0===(r=u?-r:r)?(t(0,e,s+i),t(0<1/r?0:2147483648,e,s+n)):isNaN(r)?(t(0,e,s+i),t(2146959360,e,s+n)):17976931348623157e292>>0,e,s+n)):r<22250738585072014e-324?(t((o=r/5e-324)>>>0,e,s+i),t((u<<31|o/4294967296)>>>0,e,s+n)):(t(4503599627370496*(o=r*Math.pow(2,-(r=1024===(r=Math.floor(Math.log(r)/Math.LN2))?1023:r)))>>>0,e,s+i),t((u<<31|r+1023<<20|1048576*o&1048575)>>>0,e,s+n))}function d(t,i,n,r,e){i=t(r,e+i),t=t(r,e+n),r=2*(t>>31)+1,e=t>>>20&2047,n=4294967296*(1048575&t)+i;return 2047==e?n?NaN:1/0*r:0==e?5e-324*r*n:r*Math.pow(2,e-1075)*(n+4503599627370496)}function p(t,i,n){h[0]=t,i[n]=a[0],i[n+1]=a[1],i[n+2]=a[2],i[n+3]=a[3],i[n+4]=a[4],i[n+5]=a[5],i[n+6]=a[6],i[n+7]=a[7]}function v(t,i,n){h[0]=t,i[n]=a[7],i[n+1]=a[6],i[n+2]=a[5],i[n+3]=a[4],i[n+4]=a[3],i[n+5]=a[2],i[n+6]=a[1],i[n+7]=a[0]}function b(t,i){return a[0]=t[i],a[1]=t[i+1],a[2]=t[i+2],a[3]=t[i+3],a[4]=t[i+4],a[5]=t[i+5],a[6]=t[i+6],a[7]=t[i+7],h[0]}function w(t,i){return a[7]=t[i],a[6]=t[i+1],a[5]=t[i+2],a[4]=t[i+3],a[3]=t[i+4],a[2]=t[i+5],a[1]=t[i+6],a[0]=t[i+7],h[0]}return"undefined"!=typeof Float32Array?(u=new Float32Array([-0]),f=new Uint8Array(u.buffer),c=128===f[3],t.writeFloatLE=c?r:e,t.writeFloatBE=c?e:r,t.readFloatLE=c?s:o,t.readFloatBE=c?o:s):(t.writeFloatLE=i.bind(null,y),t.writeFloatBE=i.bind(null,m),t.readFloatLE=n.bind(null,g),t.readFloatBE=n.bind(null,j)),"undefined"!=typeof Float64Array?(h=new Float64Array([-0]),a=new Uint8Array(h.buffer),c=128===a[7],t.writeDoubleLE=c?p:v,t.writeDoubleBE=c?v:p,t.readDoubleLE=c?b:w,t.readDoubleBE=c?w:b):(t.writeDoubleLE=l.bind(null,y,0,4),t.writeDoubleBE=l.bind(null,m,4,0),t.readDoubleLE=d.bind(null,g,0,4),t.readDoubleBE=d.bind(null,j,4,0)),t}function y(t,i,n){i[n]=255&t,i[n+1]=t>>>8&255,i[n+2]=t>>>16&255,i[n+3]=t>>>24}function m(t,i,n){i[n]=t>>>24,i[n+1]=t>>>16&255,i[n+2]=t>>>8&255,i[n+3]=255&t}function g(t,i){return(t[i]|t[i+1]<<8|t[i+2]<<16|t[i+3]<<24)>>>0}function j(t,i){return(t[i]<<24|t[i+1]<<16|t[i+2]<<8|t[i+3])>>>0}i.exports=r(r)},{}],7:[function(t,i,n){function r(t){try{var i=eval("require")(t);if(i&&(i.length||Object.keys(i).length))return i}catch(t){}return null}i.exports=r},{}],8:[function(t,i,n){var e=n.isAbsolute=function(t){return/^(?:\/|\w+:)/.test(t)},r=n.normalize=function(t){var i=(t=t.replace(/\\/g,"/").replace(/\/{2,}/g,"/")).split("/"),n=e(t),t="";n&&(t=i.shift()+"/");for(var r=0;r>>1,s=null,o=r;return function(t){if(t<1||e>10),s[o++]=56320+(1023&r)):s[o++]=(15&r)<<12|(63&t[i++])<<6|63&t[i++],8191>6|192:(55296==(64512&r)&&56320==(64512&(e=t.charCodeAt(o+1)))?(++o,i[n++]=(r=65536+((1023&r)<<10)+(1023&e))>>18|240,i[n++]=r>>12&63|128):i[n++]=r>>12|224,i[n++]=r>>6&63|128),i[n++]=63&r|128);return n-s}},{}],11:[function(t,i,n){i.exports=e;var r=/\/|\./;function e(t,i){r.test(t)||(t="google/protobuf/"+t+".proto",i={nested:{google:{nested:{protobuf:{nested:i}}}}}),e[t]=i}e("any",{Any:{fields:{type_url:{type:"string",id:1},value:{type:"bytes",id:2}}}}),e("duration",{Duration:i={fields:{seconds:{type:"int64",id:1},nanos:{type:"int32",id:2}}}}),e("timestamp",{Timestamp:i}),e("empty",{Empty:{fields:{}}}),e("struct",{Struct:{fields:{fields:{keyType:"string",type:"Value",id:1}}},Value:{oneofs:{kind:{oneof:["nullValue","numberValue","stringValue","boolValue","structValue","listValue"]}},fields:{nullValue:{type:"NullValue",id:1},numberValue:{type:"double",id:2},stringValue:{type:"string",id:3},boolValue:{type:"bool",id:4},structValue:{type:"Struct",id:5},listValue:{type:"ListValue",id:6}}},NullValue:{values:{NULL_VALUE:0}},ListValue:{fields:{values:{rule:"repeated",type:"Value",id:1}}}}),e("wrappers",{DoubleValue:{fields:{value:{type:"double",id:1}}},FloatValue:{fields:{value:{type:"float",id:1}}},Int64Value:{fields:{value:{type:"int64",id:1}}},UInt64Value:{fields:{value:{type:"uint64",id:1}}},Int32Value:{fields:{value:{type:"int32",id:1}}},UInt32Value:{fields:{value:{type:"uint32",id:1}}},BoolValue:{fields:{value:{type:"bool",id:1}}},StringValue:{fields:{value:{type:"string",id:1}}},BytesValue:{fields:{value:{type:"bytes",id:1}}}}),e("field_mask",{FieldMask:{fields:{paths:{rule:"repeated",type:"string",id:1}}}}),e.get=function(t){return e[t]||null}},{}],12:[function(t,i,n){var l=t(15),d=t(37);function o(t,i,n,r){var e=!1;if(i.resolvedType)if(i.resolvedType instanceof l){t("switch(d%s){",r);for(var s=i.resolvedType.values,o=Object.keys(s),u=0;u>>0",r,r);break;case"int32":case"sint32":case"sfixed32":t("m%s=d%s|0",r,r);break;case"uint64":f=!0;case"int64":case"sint64":case"fixed64":case"sfixed64":t("if(util.Long)")("(m%s=util.Long.fromValue(d%s)).unsigned=%j",r,r,f)('else if(typeof d%s==="string")',r)("m%s=parseInt(d%s,10)",r,r)('else if(typeof d%s==="number")',r)("m%s=d%s",r,r)('else if(typeof d%s==="object")',r)("m%s=new util.LongBits(d%s.low>>>0,d%s.high>>>0).toNumber(%s)",r,r,r,f?"true":"");break;case"bytes":t('if(typeof d%s==="string")',r)("util.base64.decode(d%s,m%s=util.newBuffer(util.base64.length(d%s)),0)",r,r,r)("else if(d%s.length >= 0)",r)("m%s=d%s",r,r);break;case"string":t("m%s=String(d%s)",r,r);break;case"bool":t("m%s=Boolean(d%s)",r,r)}}return t}function p(t,i,n,r){if(i.resolvedType)i.resolvedType instanceof l?t("d%s=o.enums===String?(types[%i].values[m%s]===undefined?m%s:types[%i].values[m%s]):m%s",r,n,r,r,n,r,r):t("d%s=types[%i].toObject(m%s,o)",r,n,r);else{var e=!1;switch(i.type){case"double":case"float":t("d%s=o.json&&!isFinite(m%s)?String(m%s):m%s",r,r,r,r);break;case"uint64":e=!0;case"int64":case"sint64":case"fixed64":case"sfixed64":t('if(typeof m%s==="number")',r)("d%s=o.longs===String?String(m%s):m%s",r,r,r)("else")("d%s=o.longs===String?util.Long.prototype.toString.call(m%s):o.longs===Number?new util.LongBits(m%s.low>>>0,m%s.high>>>0).toNumber(%s):m%s",r,r,r,r,e?"true":"",r);break;case"bytes":t("d%s=o.bytes===String?util.base64.encode(m%s,0,m%s.length):o.bytes===Array?Array.prototype.slice.call(m%s):m%s",r,r,r,r,r);break;default:t("d%s=m%s",r,r)}}return t}n.fromObject=function(t){var i=t.fieldsArray,n=d.codegen(["d"],t.name+"$fromObject")("if(d instanceof this.ctor)")("return d");if(!i.length)return n("return new this.ctor");n("var m=new this.ctor");for(var r=0;r>>3){");for(var n=0;n>>3){")("case 1: k=r.%s(); break",r.keyType)("case 2:"),f.basic[e]===nt?i("value=types[%i].decode(r,r.uint32())",n):i("value=r.%s()",e),i("break")("default:")("r.skipType(tag2&7)")("break")("}")("}"),f.long[r.keyType]!==nt?i('%s[typeof k==="object"?util.longToHash(k):k]=value',s):i("%s[k]=value",s)):r.repeated?(i("if(!(%s&&%s.length))",s,s)("%s=[]",s),f.packed[e]!==nt&&i("if((t&7)===2){")("var c2=r.uint32()+r.pos")("while(r.pos>>0,8|a.mapKey[s.keyType],s.keyType),f===nt?n("types[%i].encode(%s[ks[i]],w.uint32(18).fork()).ldelim().ldelim()",o,i):n(".uint32(%i).%s(%s[ks[i]]).ldelim()",16|f,u,i),n("}")("}")):s.repeated?(n("if(%s!=null&&%s.length){",i,i),s.packed&&a.packed[u]!==nt?n("w.uint32(%i).fork()",(s.id<<3|2)>>>0)("for(var i=0;i<%s.length;++i)",i)("w.%s(%s[i])",u,i)("w.ldelim()"):(n("for(var i=0;i<%s.length;++i)",i),f===nt?l(n,s,o,i+"[i]"):n("w.uint32(%i).%s(%s[i])",(s.id<<3|f)>>>0,u,i)),n("}")):(s.optional&&n("if(%s!=null&&Object.hasOwnProperty.call(m,%j))",i,s.name),f===nt?l(n,s,o,i):n("w.uint32(%i).%s(%s)",(s.id<<3|f)>>>0,u,i))}return n("return w")};var h=t(15),a=t(36),c=t(37);function l(t,i,n,r){i.resolvedType.group?t("types[%i].encode(%s,w.uint32(%i)).uint32(%i)",n,r,(i.id<<3|3)>>>0,(i.id<<3|4)>>>0):t("types[%i].encode(%s,w.uint32(%i).fork()).ldelim()",n,r,(i.id<<3|2)>>>0)}},{15:15,36:36,37:37}],15:[function(t,i,n){i.exports=s;var f=t(24),r=(((s.prototype=Object.create(f.prototype)).constructor=s).className="Enum",t(23)),e=t(37);function s(t,i,n,r,e,s){if(f.call(this,t,n),i&&"object"!=typeof i)throw TypeError("values must be an object");if(this.valuesById={},this.values=Object.create(this.valuesById),this.comment=r,this.comments=e||{},this.valuesOptions=s,this.reserved=nt,i)for(var o=Object.keys(i),u=0;ui)return!0;return!1},c.isReservedName=function(t,i){if(t)for(var n=0;n");var e=l();if(!Q.test(e))throw j(e,"name");v("=");var s=new q(g(e),A(l()),n,r);S(s,function(t){if("option"!==t)throw j(t);$(s,t),v(";")},function(){M(s)}),i.add(s);break;case"required":case"repeated":N(u,t);break;case"optional":N(u,y?"proto3_optional":"optional");break;case"oneof":e=u,n=t;if(!Q.test(n=l()))throw j(n,"name");var o=new R(g(n));S(o,function(t){"option"===t?($(o,t),v(";")):(d(t),N(o,"optional"))}),e.add(o);break;case"extensions":E(u.extensions||(u.extensions=[]));break;case"reserved":E(u.reserved||(u.reserved=[]),!0);break;default:if(!y||!Y.test(t))throw j(t);d(t),N(u,"optional")}}),t.add(u)}function N(t,i,n){var r=l();if("group"===r){var e,s,o=t,u=i,f=l();if(Q.test(f))return s=H.lcFirst(f),f===s&&(f=H.ucFirst(f)),v("="),h=A(l()),(e=new L(f)).group=!0,(s=new U(s,h,f,u)).filename=it.filename,S(e,function(t){switch(t){case"option":$(e,t),v(";");break;case"required":case"repeated":N(e,t);break;case"optional":N(e,y?"proto3_optional":"optional");break;case"message":T(e);break;case"enum":V(e);break;default:throw j(t)}}),void o.add(e).add(s);throw j(f,"name")}for(;r.endsWith(".")||p().startsWith(".");)r+=l();if(!Y.test(r))throw j(r,"type");var h=l();if(!Q.test(h))throw j(h,"name");h=g(h),v("=");var a=new U(h,A(l()),r,i,n);S(a,function(t){if("option"!==t)throw j(t);$(a,t),v(";")},function(){M(a)}),"proto3_optional"===i?(u=new R("_"+h),a.setOption("proto3_optional",!0),u.add(a),t.add(u)):t.add(a),y||!a.repeated||P.packed[r]===nt&&P.basic[r]!==nt||a.setOption("packed",!1,!0)}function V(t,i){if(!Q.test(i=l()))throw j(i,"name");var s=new z(i);S(s,function(t){switch(t){case"option":$(s,t),v(";");break;case"reserved":E(s.reserved||(s.reserved=[]),!0);break;default:var i=s,n=t;if(!Q.test(n))throw j(n,"name");v("=");var r=A(l(),!0),e={options:nt,setOption:function(t,i){this.options===nt&&(this.options={}),this.options[t]=i}};return S(e,function(t){if("option"!==t)throw j(t);$(e,t),v(";")},function(){M(e)}),void i.add(n,r,e.comment,e.options)}}),t.add(s)}function $(t,i){var n=v("(",!0);if(!Y.test(i=l()))throw j(i,"name");var r,e=i,s=e,n=(n&&(v(")"),s=e="("+e+")",i=p(),tt.test(i)&&(r=i.slice(1),e+=i,l())),v("="),function t(i,n){if(v("{",!0)){for(var r={};!v("}",!0);){if(!Q.test(h=l()))throw j(h,"name");if(null===h)throw j(h,"end of input");var e,s,o=h;if(v(":",!0),"{"===p())e=t(i,n+"."+h);else if("["===p()){if(e=[],v("[",!0)){for(;s=O(!0),e.push(s),v(",",!0););v("]"),void 0!==s&&_(i,n+"."+h,s)}}else e=O(!0),_(i,n+"."+h,e);var u=r[o];u&&(e=[].concat(u).concat(e)),r[o]=e,v(",",!0),v(";",!0)}return r}var f=O(!0);_(i,n,f);return f}(t,e));i=s,e=n,s=r,(n=t).setParsedOption&&n.setParsedOption(i,e,s)}function _(t,i,n){t.setOption&&t.setOption(i,n)}function M(t){if(v("[",!0)){for(;$(t,"option"),v(",",!0););v("]")}}for(;null!==(h=l());)switch(h){case"package":if(!w)throw j(h);if(r!==nt)throw j("package");if(r=l(),!Y.test(r))throw j(r,"name");m=m.define(r),v(";");break;case"import":if(!w)throw j(h);switch(f=u=void 0,p()){case"weak":f=s=s||[],l();break;case"public":l();default:f=e=e||[]}u=k(),v(";"),f.push(u);break;case"syntax":if(!w)throw j(h);if(v("="),o=k(),!(y="proto3"===o)&&"proto2"!==o)throw j(o,"syntax");v(";");break;case"option":$(m,h),v(";");break;default:if(x(m,h)){w=!1;continue}throw j(h)}return it.filename=null,{package:r,imports:e,weakImports:s,syntax:o,root:i}}},{15:15,16:16,20:20,22:22,25:25,29:29,33:33,34:34,35:35,36:36,37:37}],27:[function(t,i,n){i.exports=f;var r,e=t(39),s=e.LongBits,o=e.utf8;function u(t,i){return RangeError("index out of range: "+t.pos+" + "+(i||1)+" > "+t.len)}function f(t){this.buf=t,this.pos=0,this.len=t.length}function h(){return e.Buffer?function(t){return(f.create=function(t){return e.Buffer.isBuffer(t)?new r(t):c(t)})(t)}:c}var a,c="undefined"!=typeof Uint8Array?function(t){if(t instanceof Uint8Array||Array.isArray(t))return new f(t);throw Error("illegal buffer")}:function(t){if(Array.isArray(t))return new f(t);throw Error("illegal buffer")};function l(){var t=new s(0,0),i=0;if(!(4=this.len)throw u(this);if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t}return t.lo=(t.lo|(127&this.buf[this.pos++])<<7*i)>>>0,t}for(;i<4;++i)if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t;if(t.lo=(t.lo|(127&this.buf[this.pos])<<28)>>>0,t.hi=(t.hi|(127&this.buf[this.pos])>>4)>>>0,this.buf[this.pos++]<128)return t;if(i=0,4>>0,this.buf[this.pos++]<128)return t}else for(;i<5;++i){if(this.pos>=this.len)throw u(this);if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*i+3)>>>0,this.buf[this.pos++]<128)return t}throw Error("invalid varint encoding")}function d(t,i){return(t[i-4]|t[i-3]<<8|t[i-2]<<16|t[i-1]<<24)>>>0}function p(){if(this.pos+8>this.len)throw u(this,8);return new s(d(this.buf,this.pos+=4),d(this.buf,this.pos+=4))}f.create=h(),f.prototype.c=e.Array.prototype.subarray||e.Array.prototype.slice,f.prototype.uint32=(a=4294967295,function(){if(a=(127&this.buf[this.pos])>>>0,this.buf[this.pos++]<128||(a=(a|(127&this.buf[this.pos])<<7)>>>0,this.buf[this.pos++]<128||(a=(a|(127&this.buf[this.pos])<<14)>>>0,this.buf[this.pos++]<128||(a=(a|(127&this.buf[this.pos])<<21)>>>0,this.buf[this.pos++]<128||(a=(a|(15&this.buf[this.pos])<<28)>>>0,this.buf[this.pos++]<128||!((this.pos+=5)>this.len))))))return a;throw this.pos=this.len,u(this,10)}),f.prototype.int32=function(){return 0|this.uint32()},f.prototype.sint32=function(){var t=this.uint32();return t>>>1^-(1&t)|0},f.prototype.bool=function(){return 0!==this.uint32()},f.prototype.fixed32=function(){if(this.pos+4>this.len)throw u(this,4);return d(this.buf,this.pos+=4)},f.prototype.sfixed32=function(){if(this.pos+4>this.len)throw u(this,4);return 0|d(this.buf,this.pos+=4)},f.prototype.float=function(){if(this.pos+4>this.len)throw u(this,4);var t=e.float.readFloatLE(this.buf,this.pos);return this.pos+=4,t},f.prototype.double=function(){if(this.pos+8>this.len)throw u(this,4);var t=e.float.readDoubleLE(this.buf,this.pos);return this.pos+=8,t},f.prototype.bytes=function(){var t=this.uint32(),i=this.pos,n=this.pos+t;if(n>this.len)throw u(this,t);return this.pos+=t,Array.isArray(this.buf)?this.buf.slice(i,n):i===n?(t=e.Buffer)?t.alloc(0):new this.buf.constructor(0):this.c.call(this.buf,i,n)},f.prototype.string=function(){var t=this.bytes();return o.read(t,0,t.length)},f.prototype.skip=function(t){if("number"==typeof t){if(this.pos+t>this.len)throw u(this,t);this.pos+=t}else do{if(this.pos>=this.len)throw u(this)}while(128&this.buf[this.pos++]);return this},f.prototype.skipType=function(t){switch(t){case 0:this.skip();break;case 1:this.skip(8);break;case 2:this.skip(this.uint32());break;case 3:for(;4!=(t=7&this.uint32());)this.skipType(t);break;case 5:this.skip(4);break;default:throw Error("invalid wire type "+t+" at offset "+this.pos)}return this},f.u=function(t){r=t,f.create=h(),r.u();var i=e.Long?"toLong":"toNumber";e.merge(f.prototype,{int64:function(){return l.call(this)[i](!1)},uint64:function(){return l.call(this)[i](!0)},sint64:function(){return l.call(this).zzDecode()[i](!1)},fixed64:function(){return p.call(this)[i](!0)},sfixed64:function(){return p.call(this)[i](!1)}})}},{39:39}],28:[function(t,i,n){i.exports=s;var r=t(27),e=((s.prototype=Object.create(r.prototype)).constructor=s,t(39));function s(t){r.call(this,t)}s.u=function(){e.Buffer&&(s.prototype.c=e.Buffer.prototype.slice)},s.prototype.string=function(){var t=this.uint32();return this.buf.utf8Slice?this.buf.utf8Slice(this.pos,this.pos=Math.min(this.pos+t,this.len)):this.buf.toString("utf-8",this.pos,this.pos=Math.min(this.pos+t,this.len))},s.u()},{27:27,39:39}],29:[function(t,i,n){i.exports=f;var r,d,p,e=t(23),s=(((f.prototype=Object.create(e.prototype)).constructor=f).className="Root",t(16)),o=t(15),u=t(25),v=t(37);function f(t){e.call(this,"",t),this.deferred=[],this.files=[]}function b(){}f.fromJSON=function(t,i){return i=i||new f,t.options&&i.setOptions(t.options),i.addJSON(t.nested)},f.prototype.resolvePath=v.path.resolve,f.prototype.fetch=v.fetch,f.prototype.load=function t(i,s,e){"function"==typeof s&&(e=s,s=nt);var o=this;if(!e)return v.asPromise(t,o,i,s);var u=e===b;function f(t,i){if(e){if(u)throw t;var n=e;e=null,n(t,i)}}function h(t){var i=t.lastIndexOf("google/protobuf/");if(-1]/g,E=/(?:"([^"\\]*(?:\\.[^"\\]*)*)")/g,A=/(?:'([^'\\]*(?:\\.[^'\\]*)*)')/g,x=/^ *[*/]+ */,S=/^\s*\*?\/*/,T=/\n/g,N=/\s/,r=/\\(.?)/g,e={0:"\0",r:"\r",n:"\n",t:"\t"};function V(t){return t.replace(r,function(t,i){switch(i){case"\\":case"":return i;default:return e[i]||""}})}function s(h,a){h=h.toString();var c=0,l=h.length,d=1,f=0,p={},v=[],b=null;function w(t){return Error("illegal "+t+" (line "+d+")")}function y(t){return h[0|t]||""}function m(t,i,n){var r,e={type:h[0|t++]||"",lineEmpty:!1,leading:n},n=a?2:3,s=t-n;do{if(--s<0||"\n"==(r=h[0|s]||"")){e.lineEmpty=!0;break}}while(" "===r||"\t"===r);for(var o=h.substring(t,i).split(T),u=0;u>>0,this.hi=i>>>0}var s=e.zero=new e(0,0),o=(s.toNumber=function(){return 0},s.zzEncode=s.zzDecode=function(){return this},s.length=function(){return 1},e.zeroHash="\0\0\0\0\0\0\0\0",e.fromNumber=function(t){var i,n;return 0===t?s:(n=(t=(i=t<0)?-t:t)>>>0,t=(t-n)/4294967296>>>0,i&&(t=~t>>>0,n=~n>>>0,4294967295<++n&&(n=0,4294967295<++t&&(t=0))),new e(n,t))},e.from=function(t){if("number"==typeof t)return e.fromNumber(t);if(r.isString(t)){if(!r.Long)return e.fromNumber(parseInt(t,10));t=r.Long.fromString(t)}return t.low||t.high?new e(t.low>>>0,t.high>>>0):s},e.prototype.toNumber=function(t){var i;return!t&&this.hi>>>31?(t=1+~this.lo>>>0,i=~this.hi>>>0,-(t+4294967296*(i=t?i:i+1>>>0))):this.lo+4294967296*this.hi},e.prototype.toLong=function(t){return r.Long?new r.Long(0|this.lo,0|this.hi,!!t):{low:0|this.lo,high:0|this.hi,unsigned:!!t}},String.prototype.charCodeAt);e.fromHash=function(t){return"\0\0\0\0\0\0\0\0"===t?s:new e((o.call(t,0)|o.call(t,1)<<8|o.call(t,2)<<16|o.call(t,3)<<24)>>>0,(o.call(t,4)|o.call(t,5)<<8|o.call(t,6)<<16|o.call(t,7)<<24)>>>0)},e.prototype.toHash=function(){return String.fromCharCode(255&this.lo,this.lo>>>8&255,this.lo>>>16&255,this.lo>>>24,255&this.hi,this.hi>>>8&255,this.hi>>>16&255,this.hi>>>24)},e.prototype.zzEncode=function(){var t=this.hi>>31;return this.hi=((this.hi<<1|this.lo>>>31)^t)>>>0,this.lo=(this.lo<<1^t)>>>0,this},e.prototype.zzDecode=function(){var t=-(1&this.lo);return this.lo=((this.lo>>>1|this.hi<<31)^t)>>>0,this.hi=(this.hi>>>1^t)>>>0,this},e.prototype.length=function(){var t=this.lo,i=(this.lo>>>28|this.hi<<4)>>>0,n=this.hi>>>24;return 0==n?0==i?t<16384?t<128?1:2:t<2097152?3:4:i<16384?i<128?5:6:i<2097152?7:8:n<128?9:10}},{39:39}],39:[function(t,i,n){var r=n;function e(t,i,n){for(var r=Object.keys(i),e=0;e>>7|t.hi<<25)>>>0,t.hi>>>=7;for(;127>>7;i[n++]=t.lo}function b(t,i,n){i[n]=255&t,i[n+1]=t>>>8&255,i[n+2]=t>>>16&255,i[n+3]=t>>>24}c.create=l(),c.alloc=function(t){return new e.Array(t)},e.Array!==Array&&(c.alloc=e.pool(c.alloc,e.Array.prototype.subarray)),c.prototype.g=function(t,i,n){return this.tail=this.tail.next=new f(t,i,n),this.len+=i,this},(p.prototype=Object.create(f.prototype)).fn=function(t,i,n){for(;127>>=7;i[n]=t},c.prototype.uint32=function(t){return this.len+=(this.tail=this.tail.next=new p((t>>>=0)<128?1:t<16384?2:t<2097152?3:t<268435456?4:5,t)).len,this},c.prototype.int32=function(t){return t<0?this.g(v,10,s.fromNumber(t)):this.uint32(t)},c.prototype.sint32=function(t){return this.uint32((t<<1^t>>31)>>>0)},c.prototype.int64=c.prototype.uint64=function(t){t=s.from(t);return this.g(v,t.length(),t)},c.prototype.sint64=function(t){t=s.from(t).zzEncode();return this.g(v,t.length(),t)},c.prototype.bool=function(t){return this.g(d,1,t?1:0)},c.prototype.sfixed32=c.prototype.fixed32=function(t){return this.g(b,4,t>>>0)},c.prototype.sfixed64=c.prototype.fixed64=function(t){t=s.from(t);return this.g(b,4,t.lo).g(b,4,t.hi)},c.prototype.float=function(t){return this.g(e.float.writeFloatLE,4,t)},c.prototype.double=function(t){return this.g(e.float.writeDoubleLE,8,t)};var w=e.Array.prototype.set?function(t,i,n){i.set(t,n)}:function(t,i,n){for(var r=0;r>>0;return n?(e.isString(t)&&(i=c.alloc(n=o.length(t)),o.decode(t,i,0),t=i),this.uint32(n).g(w,n,t)):this.g(d,1,0)},c.prototype.string=function(t){var i=u.length(t);return i?this.uint32(i).g(u.write,i,t):this.g(d,1,0)},c.prototype.fork=function(){return this.states=new a(this),this.head=this.tail=new f(h,0,0),this.len=0,this},c.prototype.reset=function(){return this.states?(this.head=this.states.head,this.tail=this.states.tail,this.len=this.states.len,this.states=this.states.next):(this.head=this.tail=new f(h,0,0),this.len=0),this},c.prototype.ldelim=function(){var t=this.head,i=this.tail,n=this.len;return this.reset().uint32(n),n&&(this.tail.next=t.next,this.tail=i,this.len+=n),this},c.prototype.finish=function(){for(var t=this.head.next,i=this.constructor.alloc(this.len),n=0;t;)t.fn(t.val,i,n),n+=t.len,t=t.next;return i},c.u=function(t){r=t,c.create=l(),r.u()}},{39:39}],43:[function(t,i,n){i.exports=s;var r=t(42),e=((s.prototype=Object.create(r.prototype)).constructor=s,t(39));function s(){r.call(this)}function o(t,i,n){t.length<40?e.utf8.write(t,i,n):i.utf8Write?i.utf8Write(t,n):i.write(t,n)}s.u=function(){s.alloc=e.y,s.writeBytesBuffer=e.Buffer&&e.Buffer.prototype instanceof Uint8Array&&"set"===e.Buffer.prototype.set.name?function(t,i,n){i.set(t,n)}:function(t,i,n){if(t.copy)t.copy(i,n,0,t.length);else for(var r=0;r>>0;return this.uint32(i),i&&this.g(s.writeBytesBuffer,i,t),this},s.prototype.string=function(t){var i=e.Buffer.byteLength(t);return this.uint32(i),i&&this.g(o,i,t),this},s.u()},{39:39,42:42}]},{},[19])}(); -+!function(nt){"use strict";!function(r,e,t){var i=function t(i){var n=e[i];return n||r[i][0].call(n=e[i]={exports:{}},t,n,n.exports),n.exports}(t[0]);i.util.global.protobuf=i,"function"==typeof define&&define.amd&&define(["long"],function(t){return t&&t.isLong&&(i.util.Long=t,i.configure()),i}),"object"==typeof module&&module&&module.exports&&(module.exports=i)}({1:[function(t,i,n){i.exports=function(t,i){var n=Array(arguments.length-1),s=0,r=2,o=!0;for(;r>2],r=(3&f)<<4,u=1;break;case 1:s[o++]=h[r|f>>4],r=(15&f)<<2,u=2;break;case 2:s[o++]=h[r|f>>6],s[o++]=h[63&f],u=0}8191>4,r=u,s=2;break;case 2:i[n++]=(15&r)<<4|(60&u)>>2,r=u,s=3;break;case 3:i[n++]=(3&r)<<6|u,s=0}}if(1===s)throw Error(a);return n-e},n.test=function(t){return/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(t)}},{}],3:[function(t,i,n){function c(i,n){"string"==typeof i&&(n=i,i=nt);var f=[];function h(t){if("string"!=typeof t){var i=a();if(c.verbose&&console.log("codegen: "+i),i="return "+i,t){for(var n=Object.keys(t),r=Array(n.length+1),e=Array(n.length),s=0;s>>0:i<11754943508222875e-54?(e<<31|Math.round(i/1401298464324817e-60))>>>0:(e<<31|127+(t=Math.floor(Math.log(i)/Math.LN2))<<23|8388607&Math.round(i*Math.pow(2,-t)*8388608))>>>0,n,r)}function n(t,i,n){t=t(i,n),i=2*(t>>31)+1,n=t>>>23&255,t&=8388607;return 255==n?t?NaN:1/0*i:0==n?1401298464324817e-60*i*t:i*Math.pow(2,n-150)*(8388608+t)}function r(t,i,n){u[0]=t,i[n]=f[0],i[n+1]=f[1],i[n+2]=f[2],i[n+3]=f[3]}function e(t,i,n){u[0]=t,i[n]=f[3],i[n+1]=f[2],i[n+2]=f[1],i[n+3]=f[0]}function s(t,i){return f[0]=t[i],f[1]=t[i+1],f[2]=t[i+2],f[3]=t[i+3],u[0]}function o(t,i){return f[3]=t[i],f[2]=t[i+1],f[1]=t[i+2],f[0]=t[i+3],u[0]}var u,f,h,a,c;function l(t,i,n,r,e,s){var o,u=r<0?1:0;0===(r=u?-r:r)?(t(0,e,s+i),t(0<1/r?0:2147483648,e,s+n)):isNaN(r)?(t(0,e,s+i),t(2146959360,e,s+n)):17976931348623157e292>>0,e,s+n)):r<22250738585072014e-324?(t((o=r/5e-324)>>>0,e,s+i),t((u<<31|o/4294967296)>>>0,e,s+n)):(t(4503599627370496*(o=r*Math.pow(2,-(r=1024===(r=Math.floor(Math.log(r)/Math.LN2))?1023:r)))>>>0,e,s+i),t((u<<31|r+1023<<20|1048576*o&1048575)>>>0,e,s+n))}function d(t,i,n,r,e){i=t(r,e+i),t=t(r,e+n),r=2*(t>>31)+1,e=t>>>20&2047,n=4294967296*(1048575&t)+i;return 2047==e?n?NaN:1/0*r:0==e?5e-324*r*n:r*Math.pow(2,e-1075)*(n+4503599627370496)}function p(t,i,n){h[0]=t,i[n]=a[0],i[n+1]=a[1],i[n+2]=a[2],i[n+3]=a[3],i[n+4]=a[4],i[n+5]=a[5],i[n+6]=a[6],i[n+7]=a[7]}function v(t,i,n){h[0]=t,i[n]=a[7],i[n+1]=a[6],i[n+2]=a[5],i[n+3]=a[4],i[n+4]=a[3],i[n+5]=a[2],i[n+6]=a[1],i[n+7]=a[0]}function w(t,i){return a[0]=t[i],a[1]=t[i+1],a[2]=t[i+2],a[3]=t[i+3],a[4]=t[i+4],a[5]=t[i+5],a[6]=t[i+6],a[7]=t[i+7],h[0]}function b(t,i){return a[7]=t[i],a[6]=t[i+1],a[5]=t[i+2],a[4]=t[i+3],a[3]=t[i+4],a[2]=t[i+5],a[1]=t[i+6],a[0]=t[i+7],h[0]}return"undefined"!=typeof Float32Array?(u=new Float32Array([-0]),f=new Uint8Array(u.buffer),c=128===f[3],t.writeFloatLE=c?r:e,t.writeFloatBE=c?e:r,t.readFloatLE=c?s:o,t.readFloatBE=c?o:s):(t.writeFloatLE=i.bind(null,y),t.writeFloatBE=i.bind(null,m),t.readFloatLE=n.bind(null,g),t.readFloatBE=n.bind(null,j)),"undefined"!=typeof Float64Array?(h=new Float64Array([-0]),a=new Uint8Array(h.buffer),c=128===a[7],t.writeDoubleLE=c?p:v,t.writeDoubleBE=c?v:p,t.readDoubleLE=c?w:b,t.readDoubleBE=c?b:w):(t.writeDoubleLE=l.bind(null,y,0,4),t.writeDoubleBE=l.bind(null,m,4,0),t.readDoubleLE=d.bind(null,g,0,4),t.readDoubleBE=d.bind(null,j,4,0)),t}function y(t,i,n){i[n]=255&t,i[n+1]=t>>>8&255,i[n+2]=t>>>16&255,i[n+3]=t>>>24}function m(t,i,n){i[n]=t>>>24,i[n+1]=t>>>16&255,i[n+2]=t>>>8&255,i[n+3]=255&t}function g(t,i){return(t[i]|t[i+1]<<8|t[i+2]<<16|t[i+3]<<24)>>>0}function j(t,i){return(t[i]<<24|t[i+1]<<16|t[i+2]<<8|t[i+3])>>>0}i.exports=r(r)},{}],7:[function(t,i,n){function r(t){try{var i=eval("require")(t);if(i&&(i.length||Object.keys(i).length))return i}catch(t){}return null}i.exports=r},{}],8:[function(t,i,n){var e=n.isAbsolute=function(t){return/^(?:\/|\w+:)/.test(t)},r=n.normalize=function(t){var i=(t=t.replace(/\\/g,"/").replace(/\/{2,}/g,"/")).split("/"),n=e(t),t="";n&&(t=i.shift()+"/");for(var r=0;r>>1,s=null,o=r;return function(t){if(t<1||e>10),s[o++]=56320+(1023&r)):s[o++]=(15&r)<<12|(63&t[i++])<<6|63&t[i++],8191>6|192:(55296==(64512&r)&&56320==(64512&(e=t.charCodeAt(o+1)))?(++o,i[n++]=(r=65536+((1023&r)<<10)+(1023&e))>>18|240,i[n++]=r>>12&63|128):i[n++]=r>>12|224,i[n++]=r>>6&63|128),i[n++]=63&r|128);return n-s}},{}],11:[function(t,i,n){i.exports=e;var r=/\/|\./;function e(t,i){r.test(t)||(t="google/protobuf/"+t+".proto",i={nested:{google:{nested:{protobuf:{nested:i}}}}}),e[t]=i}e("any",{Any:{fields:{type_url:{type:"string",id:1},value:{type:"bytes",id:2}}}}),e("duration",{Duration:i={fields:{seconds:{type:"int64",id:1},nanos:{type:"int32",id:2}}}}),e("timestamp",{Timestamp:i}),e("empty",{Empty:{fields:{}}}),e("struct",{Struct:{fields:{fields:{keyType:"string",type:"Value",id:1}}},Value:{oneofs:{kind:{oneof:["nullValue","numberValue","stringValue","boolValue","structValue","listValue"]}},fields:{nullValue:{type:"NullValue",id:1},numberValue:{type:"double",id:2},stringValue:{type:"string",id:3},boolValue:{type:"bool",id:4},structValue:{type:"Struct",id:5},listValue:{type:"ListValue",id:6}}},NullValue:{values:{NULL_VALUE:0}},ListValue:{fields:{values:{rule:"repeated",type:"Value",id:1}}}}),e("wrappers",{DoubleValue:{fields:{value:{type:"double",id:1}}},FloatValue:{fields:{value:{type:"float",id:1}}},Int64Value:{fields:{value:{type:"int64",id:1}}},UInt64Value:{fields:{value:{type:"uint64",id:1}}},Int32Value:{fields:{value:{type:"int32",id:1}}},UInt32Value:{fields:{value:{type:"uint32",id:1}}},BoolValue:{fields:{value:{type:"bool",id:1}}},StringValue:{fields:{value:{type:"string",id:1}}},BytesValue:{fields:{value:{type:"bytes",id:1}}}}),e("field_mask",{FieldMask:{fields:{paths:{rule:"repeated",type:"string",id:1}}}}),e.get=function(t){return e[t]||null}},{}],12:[function(t,i,n){var l=t(15),d=t(37);function o(t,i,n,r){var e=!1;if(i.resolvedType)if(i.resolvedType instanceof l){t("switch(d%s){",r);for(var s=i.resolvedType.values,o=Object.keys(s),u=0;u>>0",r,r);break;case"int32":case"sint32":case"sfixed32":t("m%s=d%s|0",r,r);break;case"uint64":f=!0;case"int64":case"sint64":case"fixed64":case"sfixed64":t("if(util.Long)")("(m%s=util.Long.fromValue(d%s)).unsigned=%j",r,r,f)('else if(typeof d%s==="string")',r)("m%s=parseInt(d%s,10)",r,r)('else if(typeof d%s==="number")',r)("m%s=d%s",r,r)('else if(typeof d%s==="object")',r)("m%s=new util.LongBits(d%s.low>>>0,d%s.high>>>0).toNumber(%s)",r,r,r,f?"true":"");break;case"bytes":t('if(typeof d%s==="string")',r)("util.base64.decode(d%s,m%s=util.newBuffer(util.base64.length(d%s)),0)",r,r,r)("else if(d%s.length >= 0)",r)("m%s=d%s",r,r);break;case"string":t("m%s=String(d%s)",r,r);break;case"bool":t("m%s=Boolean(d%s)",r,r)}}return t}function p(t,i,n,r){if(i.resolvedType)i.resolvedType instanceof l?t("d%s=o.enums===String?(types[%i].values[m%s]===undefined?m%s:types[%i].values[m%s]):m%s",r,n,r,r,n,r,r):t("d%s=types[%i].toObject(m%s,o)",r,n,r);else{var e=!1;switch(i.type){case"double":case"float":t("d%s=o.json&&!isFinite(m%s)?String(m%s):m%s",r,r,r,r);break;case"uint64":e=!0;case"int64":case"sint64":case"fixed64":case"sfixed64":t('if(typeof m%s==="number")',r)("d%s=o.longs===String?String(m%s):m%s",r,r,r)("else")("d%s=o.longs===String?util.Long.prototype.toString.call(m%s):o.longs===Number?new util.LongBits(m%s.low>>>0,m%s.high>>>0).toNumber(%s):m%s",r,r,r,r,e?"true":"",r);break;case"bytes":t("d%s=o.bytes===String?util.base64.encode(m%s,0,m%s.length):o.bytes===Array?Array.prototype.slice.call(m%s):m%s",r,r,r,r,r);break;default:t("d%s=m%s",r,r)}}return t}n.fromObject=function(t){var i=t.fieldsArray,n=d.codegen(["d"],t.name+"$fromObject")("if(d instanceof this.ctor)")("return d");if(!i.length)return n("return new this.ctor");n("var m=new this.ctor");for(var r=0;r>>3){");for(var n="m"+a.safeProp("$unknownFields"),r=0;r>>3){")("case 1: k=r.%s(); break",e.keyType)("case 2:"),h.basic[s]===nt?i("value=types[%i].decode(r,r.uint32())",r):i("value=r.%s()",s),i("break")("default:")("r.skipType(tag2&7)")("break")("}")("}"),h.long[e.keyType]!==nt?i('%s[typeof k==="object"?util.longToHash(k):k]=value',o):i("%s[k]=value",o)):e.repeated?(i("if(!(%s&&%s.length))",o,o)("%s=[]",o),h.packed[s]!==nt&&i("if((t&7)===2){")("var c2=r.uint32()+r.pos")("while(r.pos>>0,8|c.mapKey[o.keyType],o.keyType),h===nt?n("types[%i].encode(%s[ks[i]],w.uint32(18).fork()).ldelim().ldelim()",u,i):n(".uint32(%i).%s(%s[ks[i]]).ldelim()",16|h,f,i),n("}")("}")):o.repeated?(n("if(%s!=null&&%s.length){",i,i),o.packed&&c.packed[f]!==nt?n("w.uint32(%i).fork()",(o.id<<3|2)>>>0)("for(var i=0;i<%s.length;++i)",i)("w.%s(%s[i])",f,i)("w.ldelim()"):(n("for(var i=0;i<%s.length;++i)",i),h===nt?d(n,o,u,i+"[i]"):n("w.uint32(%i).%s(%s[i])",(o.id<<3|h)>>>0,f,i)),n("}")):(o.optional&&n("if(%s!=null&&Object.hasOwnProperty.call(m,%j))",i,o.name),h===nt?d(n,o,u,i):n("w.uint32(%i).%s(%s)",(o.id<<3|h)>>>0,f,i))}return n("for(var i=0;i>>0,(i.id<<3|4)>>>0):t("types[%i].encode(%s,w.uint32(%i).fork()).ldelim()",n,r,(i.id<<3|2)>>>0)}},{15:15,36:36,37:37}],15:[function(t,i,n){i.exports=s;var f=t(24),r=(((s.prototype=Object.create(f.prototype)).constructor=s).className="Enum",t(23)),e=t(37);function s(t,i,n,r,e,s){if(f.call(this,t,n),i&&"object"!=typeof i)throw TypeError("values must be an object");if(this.valuesById={},this.values=Object.create(this.valuesById),this.comment=r,this.comments=e||{},this.valuesOptions=s,this.reserved=nt,i)for(var o=Object.keys(i),u=0;ui)return!0;return!1},c.isReservedName=function(t,i){if(t)for(var n=0;n");var e=l();if(!Q.test(e))throw j(e,"name");v("=");var s=new q(g(e),A(l()),n,r);S(s,function(t){if("option"!==t)throw j(t);V(s,t),v(";")},function(){M(s)}),i.add(s);break;case"required":case"repeated":N(u,t);break;case"optional":N(u,y?"proto3_optional":"optional");break;case"oneof":e=u,n=t;if(!Q.test(n=l()))throw j(n,"name");var o=new R(g(n));S(o,function(t){"option"===t?(V(o,t),v(";")):(d(t),N(o,"optional"))}),e.add(o);break;case"extensions":E(u.extensions||(u.extensions=[]));break;case"reserved":E(u.reserved||(u.reserved=[]),!0);break;default:if(!y||!Y.test(t))throw j(t);d(t),N(u,"optional")}}),t.add(u)}function N(t,i,n){var r=l();if("group"===r){var e,s,o=t,u=i,f=l();if(Q.test(f))return s=H.lcFirst(f),f===s&&(f=H.ucFirst(f)),v("="),h=A(l()),(e=new I(f)).group=!0,(s=new L(s,h,f,u)).filename=it.filename,S(e,function(t){switch(t){case"option":V(e,t),v(";");break;case"required":case"repeated":N(e,t);break;case"optional":N(e,y?"proto3_optional":"optional");break;case"message":T(e);break;case"enum":$(e);break;default:throw j(t)}}),void o.add(e).add(s);throw j(f,"name")}for(;r.endsWith(".")||p().startsWith(".");)r+=l();if(!Y.test(r))throw j(r,"type");var h=l();if(!Q.test(h))throw j(h,"name");h=g(h),v("=");var a=new L(h,A(l()),r,i,n);S(a,function(t){if("option"!==t)throw j(t);V(a,t),v(";")},function(){M(a)}),"proto3_optional"===i?(u=new R("_"+h),a.setOption("proto3_optional",!0),u.add(a),t.add(u)):t.add(a),y||!a.repeated||B.packed[r]===nt&&B.basic[r]!==nt||a.setOption("packed",!1,!0)}function $(t,i){if(!Q.test(i=l()))throw j(i,"name");var s=new z(i);S(s,function(t){switch(t){case"option":V(s,t),v(";");break;case"reserved":E(s.reserved||(s.reserved=[]),!0);break;default:var i=s,n=t;if(!Q.test(n))throw j(n,"name");v("=");var r=A(l(),!0),e={options:nt,setOption:function(t,i){this.options===nt&&(this.options={}),this.options[t]=i}};return S(e,function(t){if("option"!==t)throw j(t);V(e,t),v(";")},function(){M(e)}),void i.add(n,r,e.comment,e.options)}}),t.add(s)}function V(t,i){var n=v("(",!0);if(!Y.test(i=l()))throw j(i,"name");var r,e=i,s=e,n=(n&&(v(")"),s=e="("+e+")",i=p(),tt.test(i)&&(r=i.slice(1),e+=i,l())),v("="),function t(i,n){if(v("{",!0)){for(var r={};!v("}",!0);){if(!Q.test(h=l()))throw j(h,"name");if(null===h)throw j(h,"end of input");var e,s,o=h;if(v(":",!0),"{"===p())e=t(i,n+"."+h);else if("["===p()){if(e=[],v("[",!0)){for(;s=O(!0),e.push(s),v(",",!0););v("]"),void 0!==s&&_(i,n+"."+h,s)}}else e=O(!0),_(i,n+"."+h,e);var u=r[o];u&&(e=[].concat(u).concat(e)),r[o]=e,v(",",!0),v(";",!0)}return r}var f=O(!0);_(i,n,f);return f}(t,e));i=s,e=n,s=r,(n=t).setParsedOption&&n.setParsedOption(i,e,s)}function _(t,i,n){t.setOption&&t.setOption(i,n)}function M(t){if(v("[",!0)){for(;V(t,"option"),v(",",!0););v("]")}}for(;null!==(h=l());)switch(h){case"package":if(!b)throw j(h);if(r!==nt)throw j("package");if(r=l(),!Y.test(r))throw j(r,"name");m=m.define(r),v(";");break;case"import":if(!b)throw j(h);switch(f=u=void 0,p()){case"weak":f=s=s||[],l();break;case"public":l();default:f=e=e||[]}u=k(),v(";"),f.push(u);break;case"syntax":if(!b)throw j(h);if(v("="),o=k(),!(y="proto3"===o)&&"proto2"!==o)throw j(o,"syntax");v(";");break;case"option":V(m,h),v(";");break;default:if(x(m,h)){b=!1;continue}throw j(h)}return it.filename=null,{package:r,imports:e,weakImports:s,syntax:o,root:i}}},{15:15,16:16,20:20,22:22,25:25,29:29,33:33,34:34,35:35,36:36,37:37}],27:[function(t,i,n){i.exports=f;var r,e=t(39),s=e.LongBits,o=e.utf8;function u(t,i){return RangeError("index out of range: "+t.pos+" + "+(i||1)+" > "+t.len)}function f(t){this.buf=t,this.pos=0,this.len=t.length}function h(){return e.Buffer?function(t){return(f.create=function(t){return e.Buffer.isBuffer(t)?new r(t):c(t)})(t)}:c}var a,c="undefined"!=typeof Uint8Array?function(t){if(t instanceof Uint8Array||Array.isArray(t))return new f(t);throw Error("illegal buffer")}:function(t){if(Array.isArray(t))return new f(t);throw Error("illegal buffer")};function l(){var t=new s(0,0),i=0;if(!(4=this.len)throw u(this);if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t}return t.lo=(t.lo|(127&this.buf[this.pos++])<<7*i)>>>0,t}for(;i<4;++i)if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*i)>>>0,this.buf[this.pos++]<128)return t;if(t.lo=(t.lo|(127&this.buf[this.pos])<<28)>>>0,t.hi=(t.hi|(127&this.buf[this.pos])>>4)>>>0,this.buf[this.pos++]<128)return t;if(i=0,4>>0,this.buf[this.pos++]<128)return t}else for(;i<5;++i){if(this.pos>=this.len)throw u(this);if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*i+3)>>>0,this.buf[this.pos++]<128)return t}throw Error("invalid varint encoding")}function d(t,i){return(t[i-4]|t[i-3]<<8|t[i-2]<<16|t[i-1]<<24)>>>0}function p(){if(this.pos+8>this.len)throw u(this,8);return new s(d(this.buf,this.pos+=4),d(this.buf,this.pos+=4))}f.create=h(),f.prototype.c=e.Array.prototype.subarray||e.Array.prototype.slice,f.prototype.uint32=(a=4294967295,function(){if(a=(127&this.buf[this.pos])>>>0,this.buf[this.pos++]<128||(a=(a|(127&this.buf[this.pos])<<7)>>>0,this.buf[this.pos++]<128||(a=(a|(127&this.buf[this.pos])<<14)>>>0,this.buf[this.pos++]<128||(a=(a|(127&this.buf[this.pos])<<21)>>>0,this.buf[this.pos++]<128||(a=(a|(15&this.buf[this.pos])<<28)>>>0,this.buf[this.pos++]<128||!((this.pos+=5)>this.len))))))return a;throw this.pos=this.len,u(this,10)}),f.prototype.int32=function(){return 0|this.uint32()},f.prototype.sint32=function(){var t=this.uint32();return t>>>1^-(1&t)|0},f.prototype.bool=function(){return 0!==this.uint32()},f.prototype.fixed32=function(){if(this.pos+4>this.len)throw u(this,4);return d(this.buf,this.pos+=4)},f.prototype.sfixed32=function(){if(this.pos+4>this.len)throw u(this,4);return 0|d(this.buf,this.pos+=4)},f.prototype.float=function(){if(this.pos+4>this.len)throw u(this,4);var t=e.float.readFloatLE(this.buf,this.pos);return this.pos+=4,t},f.prototype.double=function(){if(this.pos+8>this.len)throw u(this,4);var t=e.float.readDoubleLE(this.buf,this.pos);return this.pos+=8,t},f.prototype.bytes=function(){var t=this.uint32(),i=this.pos,n=this.pos+t;if(n>this.len)throw u(this,t);return this.pos+=t,Array.isArray(this.buf)?this.buf.slice(i,n):i===n?(t=e.Buffer)?t.alloc(0):new this.buf.constructor(0):this.c.call(this.buf,i,n)},f.prototype.string=function(){var t=this.bytes();return o.read(t,0,t.length)},f.prototype.skip=function(t){if("number"==typeof t){if(this.pos+t>this.len)throw u(this,t);this.pos+=t}else do{if(this.pos>=this.len)throw u(this)}while(128&this.buf[this.pos++]);return this},f.prototype.skipType=function(t){switch(t){case 0:this.skip();break;case 1:this.skip(8);break;case 2:this.skip(this.uint32());break;case 3:for(;4!=(t=7&this.uint32());)this.skipType(t);break;case 5:this.skip(4);break;default:throw Error("invalid wire type "+t+" at offset "+this.pos)}return this},f.u=function(t){r=t,f.create=h(),r.u();var i=e.Long?"toLong":"toNumber";e.merge(f.prototype,{int64:function(){return l.call(this)[i](!1)},uint64:function(){return l.call(this)[i](!0)},sint64:function(){return l.call(this).zzDecode()[i](!1)},fixed64:function(){return p.call(this)[i](!0)},sfixed64:function(){return p.call(this)[i](!1)}})}},{39:39}],28:[function(t,i,n){i.exports=s;var r=t(27),e=((s.prototype=Object.create(r.prototype)).constructor=s,t(39));function s(t){r.call(this,t)}s.u=function(){e.Buffer&&(s.prototype.c=e.Buffer.prototype.slice)},s.prototype.string=function(){var t=this.uint32();return this.buf.utf8Slice?this.buf.utf8Slice(this.pos,this.pos=Math.min(this.pos+t,this.len)):this.buf.toString("utf-8",this.pos,this.pos=Math.min(this.pos+t,this.len))},s.u()},{27:27,39:39}],29:[function(t,i,n){i.exports=f;var r,d,p,e=t(23),s=(((f.prototype=Object.create(e.prototype)).constructor=f).className="Root",t(16)),o=t(15),u=t(25),v=t(37);function f(t){e.call(this,"",t),this.deferred=[],this.files=[]}function w(){}f.fromJSON=function(t,i){return i=i||new f,t.options&&i.setOptions(t.options),i.addJSON(t.nested)},f.prototype.resolvePath=v.path.resolve,f.prototype.fetch=v.fetch,f.prototype.load=function t(i,s,e){"function"==typeof s&&(e=s,s=nt);var o=this;if(!e)return v.asPromise(t,o,i,s);var u=e===w;function f(t,i){if(e){if(u)throw t;var n=e;e=null,n(t,i)}}function h(t){var i=t.lastIndexOf("google/protobuf/");if(-1]/g,E=/(?:"([^"\\]*(?:\\.[^"\\]*)*)")/g,A=/(?:'([^'\\]*(?:\\.[^'\\]*)*)')/g,x=/^ *[*/]+ */,S=/^\s*\*?\/*/,T=/\n/g,N=/\s/,r=/\\(.?)/g,e={0:"\0",r:"\r",n:"\n",t:"\t"};function $(t){return t.replace(r,function(t,i){switch(i){case"\\":case"":return i;default:return e[i]||""}})}function s(h,a){h=h.toString();var c=0,l=h.length,d=1,f=0,p={},v=[],w=null;function b(t){return Error("illegal "+t+" (line "+d+")")}function y(t){return h[0|t]||""}function m(t,i,n){var r,e={type:h[0|t++]||"",lineEmpty:!1,leading:n},n=a?2:3,s=t-n;do{if(--s<0||"\n"==(r=h[0|s]||"")){e.lineEmpty=!0;break}}while(" "===r||"\t"===r);for(var o=h.substring(t,i).split(T),u=0;u>>0,this.hi=i>>>0}var s=e.zero=new e(0,0),o=(s.toNumber=function(){return 0},s.zzEncode=s.zzDecode=function(){return this},s.length=function(){return 1},e.zeroHash="\0\0\0\0\0\0\0\0",e.fromNumber=function(t){var i,n;return 0===t?s:(n=(t=(i=t<0)?-t:t)>>>0,t=(t-n)/4294967296>>>0,i&&(t=~t>>>0,n=~n>>>0,4294967295<++n&&(n=0,4294967295<++t&&(t=0))),new e(n,t))},e.from=function(t){if("number"==typeof t)return e.fromNumber(t);if(r.isString(t)){if(!r.Long)return e.fromNumber(parseInt(t,10));t=r.Long.fromString(t)}return t.low||t.high?new e(t.low>>>0,t.high>>>0):s},e.prototype.toNumber=function(t){var i;return!t&&this.hi>>>31?(t=1+~this.lo>>>0,i=~this.hi>>>0,-(t+4294967296*(i=t?i:i+1>>>0))):this.lo+4294967296*this.hi},e.prototype.toLong=function(t){return r.Long?new r.Long(0|this.lo,0|this.hi,!!t):{low:0|this.lo,high:0|this.hi,unsigned:!!t}},String.prototype.charCodeAt);e.fromHash=function(t){return"\0\0\0\0\0\0\0\0"===t?s:new e((o.call(t,0)|o.call(t,1)<<8|o.call(t,2)<<16|o.call(t,3)<<24)>>>0,(o.call(t,4)|o.call(t,5)<<8|o.call(t,6)<<16|o.call(t,7)<<24)>>>0)},e.prototype.toHash=function(){return String.fromCharCode(255&this.lo,this.lo>>>8&255,this.lo>>>16&255,this.lo>>>24,255&this.hi,this.hi>>>8&255,this.hi>>>16&255,this.hi>>>24)},e.prototype.zzEncode=function(){var t=this.hi>>31;return this.hi=((this.hi<<1|this.lo>>>31)^t)>>>0,this.lo=(this.lo<<1^t)>>>0,this},e.prototype.zzDecode=function(){var t=-(1&this.lo);return this.lo=((this.lo>>>1|this.hi<<31)^t)>>>0,this.hi=(this.hi>>>1^t)>>>0,this},e.prototype.length=function(){var t=this.lo,i=(this.lo>>>28|this.hi<<4)>>>0,n=this.hi>>>24;return 0==n?0==i?t<16384?t<128?1:2:t<2097152?3:4:i<16384?i<128?5:6:i<2097152?7:8:n<128?9:10}},{39:39}],39:[function(t,i,n){var r=n;function e(t,i,n){for(var r=Object.keys(i),e=0;e>>7|t.hi<<25)>>>0,t.hi>>>=7;for(;127>>7;i[n++]=t.lo}function w(t,i,n){i[n]=255&t,i[n+1]=t>>>8&255,i[n+2]=t>>>16&255,i[n+3]=t>>>24}c.create=l(),c.alloc=function(t){return new e.Array(t)},e.Array!==Array&&(c.alloc=e.pool(c.alloc,e.Array.prototype.subarray)),c.prototype.g=function(t,i,n){return this.tail=this.tail.next=new f(t,i,n),this.len+=i,this},(p.prototype=Object.create(f.prototype)).fn=function(t,i,n){for(;127>>=7;i[n]=t},c.prototype.uint32=function(t){return this.len+=(this.tail=this.tail.next=new p((t>>>=0)<128?1:t<16384?2:t<2097152?3:t<268435456?4:5,t)).len,this},c.prototype.int32=function(t){return t<0?this.g(v,10,s.fromNumber(t)):this.uint32(t)},c.prototype.sint32=function(t){return this.uint32((t<<1^t>>31)>>>0)},c.prototype.int64=c.prototype.uint64=function(t){t=s.from(t);return this.g(v,t.length(),t)},c.prototype.sint64=function(t){t=s.from(t).zzEncode();return this.g(v,t.length(),t)},c.prototype.bool=function(t){return this.g(d,1,t?1:0)},c.prototype.sfixed32=c.prototype.fixed32=function(t){return this.g(w,4,t>>>0)},c.prototype.sfixed64=c.prototype.fixed64=function(t){t=s.from(t);return this.g(w,4,t.lo).g(w,4,t.hi)},c.prototype.float=function(t){return this.g(e.float.writeFloatLE,4,t)},c.prototype.double=function(t){return this.g(e.float.writeDoubleLE,8,t)};var b=e.Array.prototype.set?function(t,i,n){i.set(t,n)}:function(t,i,n){for(var r=0;r>>0;return n?(e.isString(t)&&(i=c.alloc(n=o.length(t)),o.decode(t,i,0),t=i),this.uint32(n).g(b,n,t)):this.g(d,1,0)},c.prototype.j=function(t){return this.g(b,t.length,t)},c.prototype.string=function(t){var i=u.length(t);return i?this.uint32(i).g(u.write,i,t):this.g(d,1,0)},c.prototype.fork=function(){return this.states=new a(this),this.head=this.tail=new f(h,0,0),this.len=0,this},c.prototype.reset=function(){return this.states?(this.head=this.states.head,this.tail=this.states.tail,this.len=this.states.len,this.states=this.states.next):(this.head=this.tail=new f(h,0,0),this.len=0),this},c.prototype.ldelim=function(){var t=this.head,i=this.tail,n=this.len;return this.reset().uint32(n),n&&(this.tail.next=t.next,this.tail=i,this.len+=n),this},c.prototype.finish=function(){for(var t=this.head.next,i=this.constructor.alloc(this.len),n=0;t;)t.fn(t.val,i,n),n+=t.len,t=t.next;return i},c.u=function(t){r=t,c.create=l(),r.u()}},{39:39}],43:[function(t,i,n){i.exports=s;var r=t(42),e=((s.prototype=Object.create(r.prototype)).constructor=s,t(39));function s(){r.call(this)}function o(t,i,n){t.length<40?e.utf8.write(t,i,n):i.utf8Write?i.utf8Write(t,n):i.write(t,n)}s.u=function(){s.alloc=e.y,s.writeBytesBuffer=e.Buffer&&e.Buffer.prototype instanceof Uint8Array&&"set"===e.Buffer.prototype.set.name?function(t,i,n){i.set(t,n)}:function(t,i,n){if(t.copy)t.copy(i,n,0,t.length);else for(var r=0;r>>0;return this.uint32(i),i&&this.g(s.writeBytesBuffer,i,t),this},s.prototype.string=function(t){var i=e.Buffer.byteLength(t);return this.uint32(i),i&&this.g(o,i,t),this},s.u()},{39:39,42:42}]},{},[19])}(); - //# sourceMappingURL=protobuf.min.js.map -diff --git a/ext/descriptor/index.d.ts b/ext/descriptor/index.d.ts -index 7b6c20b..da2a2c0 100644 ---- a/ext/descriptor/index.d.ts -+++ b/ext/descriptor/index.d.ts -@@ -1,4 +1,5 @@ - import * as $protobuf from "../.."; -+import Long = require("long"); - export const FileDescriptorSet: $protobuf.Type; - - export const FileDescriptorProto: $protobuf.Type; -@@ -58,6 +59,8 @@ export const GeneratedCodeInfo: $protobuf.Type & { - - export interface IFileDescriptorSet { - file: IFileDescriptorProto[]; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IFileDescriptorProto { -@@ -73,6 +76,8 @@ export interface IFileDescriptorProto { - options?: IFileOptions; - sourceCodeInfo?: any; - syntax?: string; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IFileOptions { -@@ -90,6 +95,8 @@ export interface IFileOptions { - ccEnableArenas?: boolean; - objcClassPrefix?: string; - csharpNamespace?: string; -+ -+ $unknownFields?: ReadonlyArray; - } - - type IFileOptionsOptimizeMode = number; -@@ -105,20 +112,28 @@ export interface IDescriptorProto { - options?: IMessageOptions; - reservedRange?: IDescriptorProtoReservedRange[]; - reservedName?: string[]; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IMessageOptions { - mapEntry?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IDescriptorProtoExtensionRange { - start?: number; - end?: number; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IDescriptorProtoReservedRange { - start?: number; - end?: number; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IFieldDescriptorProto { -@@ -132,6 +147,8 @@ export interface IFieldDescriptorProto { - oneofIndex?: number; - jsonName?: any; - options?: IFieldOptions; -+ -+ $unknownFields?: ReadonlyArray; - } - - type IFieldDescriptorProtoLabel = number; -@@ -141,6 +158,8 @@ type IFieldDescriptorProtoType = number; - export interface IFieldOptions { - packed?: boolean; - jstype?: IFieldOptionsJSType; -+ -+ $unknownFields?: ReadonlyArray; - } - - type IFieldOptionsJSType = number; -@@ -149,32 +168,44 @@ export interface IEnumDescriptorProto { - name?: string; - value?: IEnumValueDescriptorProto[]; - options?: IEnumOptions; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IEnumValueDescriptorProto { - name?: string; - number?: number; - options?: any; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IEnumOptions { - allowAlias?: boolean; - deprecated?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IOneofDescriptorProto { - name?: string; - options?: any; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IServiceDescriptorProto { - name?: string; - method?: IMethodDescriptorProto[]; - options?: IServiceOptions; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IServiceOptions { - deprecated?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IMethodDescriptorProto { -@@ -184,8 +215,12 @@ export interface IMethodDescriptorProto { - options?: IMethodOptions; - clientStreaming?: boolean; - serverStreaming?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - export interface IMethodOptions { - deprecated?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } -diff --git a/index.d.ts b/index.d.ts -index dcbdabb..f5a4f8c 100644 ---- a/index.d.ts -+++ b/index.d.ts -@@ -16,27 +16,37 @@ export namespace common { - interface IAny { - typeUrl?: string; - bytes?: Uint8Array; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.Duration message. */ - interface IDuration { - seconds?: (number|Long); - nanos?: number; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.Timestamp message. */ - interface ITimestamp { - seconds?: (number|Long); - nanos?: number; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.Empty message. */ - interface IEmpty { -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.Struct message. */ - interface IStruct { - fields?: { [k: string]: IValue }; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.Value message. */ -@@ -48,56 +58,78 @@ export namespace common { - boolValue?: boolean; - structValue?: IStruct; - listValue?: IListValue; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.ListValue message. */ - interface IListValue { - values?: IValue[]; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.DoubleValue message. */ - interface IDoubleValue { - value?: number; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.FloatValue message. */ - interface IFloatValue { - value?: number; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.Int64Value message. */ - interface IInt64Value { - value?: (number|Long); -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.UInt64Value message. */ - interface IUInt64Value { - value?: (number|Long); -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.Int32Value message. */ - interface IInt32Value { - value?: number; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.UInt32Value message. */ - interface IUInt32Value { - value?: number; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.BoolValue message. */ - interface IBoolValue { - value?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.StringValue message. */ - interface IStringValue { - value?: string; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties of a google.protobuf.BytesValue message. */ - interface IBytesValue { - value?: Uint8Array; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -@@ -232,6 +264,8 @@ export class Enum extends ReflectionObject { - * @returns `true` if reserved, otherwise `false` - */ - public isReservedName(name: string): boolean; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Enum descriptor. */ -@@ -242,6 +276,8 @@ export interface IEnum { - - /** Enum options */ - options?: { [k: string]: any }; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Reflected message field. */ -@@ -288,6 +324,8 @@ export class Field extends FieldBase { - * @returns Decorator function - */ - public static d>(fieldId: number, fieldType: (Constructor|string), fieldRule?: ("optional"|"required"|"repeated")): FieldDecorator; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Base class of all reflected message fields. This is not an actual class but here for the sake of having consistent type definitions. */ -@@ -369,6 +407,8 @@ export class FieldBase extends ReflectionObject { - * @throws {Error} If any reference cannot be resolved - */ - public resolve(): Field; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Field descriptor. */ -@@ -385,6 +425,8 @@ export interface IField { - - /** Field options */ - options?: { [k: string]: any }; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Extension field descriptor. */ -@@ -392,6 +434,8 @@ export interface IExtensionField extends IField { - - /** Extended type */ - extend: string; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -@@ -494,6 +538,8 @@ export class MapField extends FieldBase { - * @returns Decorator function - */ - public static d }>(fieldId: number, fieldKeyType: ("int32"|"uint32"|"sint32"|"fixed32"|"sfixed32"|"int64"|"uint64"|"sint64"|"fixed64"|"sfixed64"|"bool"|"string"), fieldValueType: ("double"|"float"|"int32"|"uint32"|"sint32"|"fixed32"|"sfixed32"|"int64"|"uint64"|"sint64"|"fixed64"|"sfixed64"|"bool"|"string"|"bytes"|object|Constructor<{}>)): FieldDecorator; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Map field descriptor. */ -@@ -501,6 +547,8 @@ export interface IMapField extends IField { - - /** Key type */ - keyType: string; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Extension map field descriptor. */ -@@ -508,6 +556,8 @@ export interface IExtensionMapField extends IMapField { - - /** Extended type */ - extend: string; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Abstract runtime message. */ -@@ -589,6 +639,8 @@ export class Message { - * @returns JSON object - */ - public toJSON(): { [k: string]: any }; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Reflected service method. */ -@@ -650,6 +702,8 @@ export class Method extends ReflectionObject { - * @returns Method descriptor - */ - public toJSON(toJSONOptions?: IToJSONOptions): IMethod; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Method descriptor. */ -@@ -678,6 +732,8 @@ export interface IMethod { - - /** Method options properly parsed into an object */ - parsedOptions?: { [k: string]: any }; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Reflected namespace. */ -@@ -722,6 +778,8 @@ export class Namespace extends NamespaceBase { - * @returns `true` if reserved, otherwise `false` - */ - public static isReservedName(reserved: ((number[]|string)[]|undefined), name: string): boolean; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Base class of all reflection objects containing nested objects. This is not an actual class but here for the sake of having consistent type definitions. */ -@@ -847,6 +905,8 @@ export abstract class NamespaceBase extends ReflectionObject { - * @throws {Error} If `path` does not point to a service - */ - public lookupService(path: (string|string[])): Service; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Namespace descriptor. */ -@@ -857,6 +917,8 @@ export interface INamespace { - - /** Nested object descriptors */ - nested?: { [k: string]: AnyNestedObject }; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Any extension field descriptor. */ -@@ -957,6 +1019,8 @@ export abstract class ReflectionObject { - * @returns Class name[, space, full name] - */ - public toString(): string; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Reflected oneof. */ -@@ -1016,6 +1080,8 @@ export class OneOf extends ReflectionObject { - * @returns Decorator function - */ - public static d(...fieldNames: string[]): OneOfDecorator; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Oneof descriptor. */ -@@ -1026,6 +1092,8 @@ export interface IOneOf { - - /** Oneof options */ - options?: { [k: string]: any }; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -@@ -1060,6 +1128,8 @@ export interface IParserResult { - - /** Populated root instance */ - root: Root; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Options modifying the behavior of {@link parse}. */ -@@ -1073,6 +1143,8 @@ export interface IParseOptions { - - /** Use trailing comment when both leading comment and trailing comment exist. */ - preferTrailingComment?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Options modifying the behavior of JSON serialization. */ -@@ -1080,6 +1152,8 @@ export interface IToJSONOptions { - - /** Serializes comments. */ - keepComments?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -@@ -1220,6 +1294,8 @@ export class Reader { - * @returns `this` - */ - public skipType(wireType: number): Reader; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Wire format reader using node buffers. */ -@@ -1236,6 +1312,8 @@ export class BufferReader extends Reader { - * @returns Value read - */ - public bytes(): Buffer; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Root namespace wrapping all types, enums, services, sub-namespaces etc. that belong together. */ -@@ -1309,6 +1387,8 @@ export class Root extends NamespaceBase { - * @throws {Error} If synchronous fetching is not supported (i.e. in browsers) or if a file's syntax is invalid - */ - public loadSync(filename: (string|string[]), options?: IParseOptions): Root; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** -@@ -1374,6 +1454,8 @@ export namespace rpc { - * @returns `this` - */ - public end(endedByRPC?: boolean): rpc.Service; -+ -+ public $unknownFields?: ReadonlyArray; - } - } - -@@ -1433,6 +1515,8 @@ export class Service extends NamespaceBase { - * @returns RPC service. Useful where requests and/or responses are streamed. - */ - public create(rpcImpl: RPCImpl, requestDelimited?: boolean, responseDelimited?: boolean): rpc.Service; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Service descriptor. */ -@@ -1440,6 +1524,8 @@ export interface IService extends INamespace { - - /** Method descriptors */ - methods: { [k: string]: IMethod }; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -@@ -1496,6 +1582,8 @@ export interface ITokenizerHandle { - - /** Current line number */ - line: number; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -@@ -1683,6 +1771,8 @@ export class Type extends NamespaceBase { - * @returns Decorator function - */ - public static d>(typeName?: string): TypeDecorator; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Message type descriptor. */ -@@ -1702,6 +1792,8 @@ export interface IType extends INamespace { - - /** Whether a legacy group or not */ - group?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Conversion options as used by {@link Type#toObject} and {@link Message.toObject}. */ -@@ -1742,6 +1834,8 @@ export interface IConversionOptions { - - /** Performs additional JSON compatibility conversions, i.e. NaN and Infinity to strings */ - json?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -@@ -1838,6 +1932,8 @@ export namespace types { - /** Constructor type. */ - export interface Constructor extends Function { - new(...params: any[]): T; prototype: T; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Properties type. */ -@@ -1848,6 +1944,8 @@ type Properties = { [P in keyof T]?: T[P] }; - * This is a minimal stand-alone definition of a Buffer instance. The actual type is that exported by node's typings. - */ - export interface Buffer extends Uint8Array { -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -@@ -1864,6 +1962,8 @@ export interface Long { - - /** Whether unsigned or not */ - unsigned: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -@@ -1961,6 +2061,8 @@ export namespace util { - * @returns Length - */ - public length(): number; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Whether running within node or not. */ -@@ -2088,6 +2190,8 @@ export namespace util { - - /** So far decoded message instance. */ - public instance: Message; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** -@@ -2298,6 +2402,8 @@ export namespace util { - * @returns `this` - */ - public emit(evt: string, ...args: any[]): this; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Reads / writes floats / doubles from / to buffers. */ -@@ -2497,6 +2603,8 @@ export interface IWrapper { - - /** To object converter */ - toObject?: WrapperToObjectConverter; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** Wire format writer using `Uint8Array` if available, otherwise `Array`. */ -@@ -2664,6 +2772,8 @@ export class Writer { - * @returns Finished buffer - */ - public finish(): Uint8Array; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** Wire format writer using node buffers. */ -@@ -2684,6 +2794,8 @@ export class BufferWriter extends Writer { - * @returns Finished buffer - */ - public finish(): Buffer; -+ -+ public $unknownFields?: ReadonlyArray; - } - - /** -@@ -2723,6 +2835,8 @@ export interface IFetchOptions { - - /** If `true`, forces the use of XMLHttpRequest */ - xhr?: boolean; -+ -+ $unknownFields?: ReadonlyArray; - } - - /** -diff --git a/src/decoder.js b/src/decoder.js -index b421bb4..31aa011 100644 ---- a/src/decoder.js -+++ b/src/decoder.js -@@ -21,6 +21,7 @@ function decoder(mtype) { - ("r=Reader.create(r)") - ("var c=l===undefined?r.len:r.pos+l,m=new this.ctor" + (mtype.fieldsArray.filter(function(field) { return field.map; }).length ? ",k,value" : "")) - ("while(r.pos>>3){"); - -+ var unknownRef = "m" + util.safeProp("$unknownFields"); -+ - var i = 0; - for (; i < /* initializes */ mtype.fieldsArray.length; ++i) { - var field = mtype._fieldsArray[i].resolve(), -@@ -110,6 +113,11 @@ function decoder(mtype) { - } gen - ("default:") - ("r.skipType(t&7)") -+ ("if (!(%s)) {", unknownRef) -+ ("%s = []", unknownRef) -+ ("}") -+ -+ ("%s.push(r.buf.slice(unknownStartPos, r.pos))", unknownRef) - ("break") - - ("}") -diff --git a/src/encoder.js b/src/encoder.js -index 52a3b01..48d2fd3 100644 ---- a/src/encoder.js -+++ b/src/encoder.js -@@ -36,6 +36,21 @@ function encoder(mtype) { - // "when a message is serialized its known fields should be written sequentially by field number" - var fields = /* initializes */ mtype.fieldsArray.slice().sort(util.compareFieldsById); - -+ var unknownRef = "m" + util.safeProp("$unknownFields"); -+ -+ // Redecode unknown fields and apply them to the message before encoding -+ gen -+ ("var fullyUnknown=[]") -+ ("if(%s&&this.ctor.decode) {", unknownRef) -+ ("for(var i=0;i<%s.length;++i) {", unknownRef) -+ ("try {") -+ ("var known=this.ctor.decode(%s[i])", unknownRef) -+ ("fullyUnknown=fullyUnknown.concat(known.$unknownFields||[])") -+ ("m=Object.assign(known,m)") -+ ("}catch(_){}") -+ ("}") -+ ("}"); -+ - for (var i = 0; i < fields.length; ++i) { - var field = fields[i].resolve(), - index = mtype._fieldsArray.indexOf(field), -@@ -94,6 +109,11 @@ function encoder(mtype) { - } - } - -+ gen -+ ("for(var i=0;i;"); -+ } else { -+ writeln("public $unknownFields?: ReadonlyArray;"); -+ } -+ - --indent; - writeln("}"); - diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 093a52b1c8..b44bc61099 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,12 +60,6 @@ patchedDependencies: node-fetch@2.6.7: hash: 94385e2da301d6873723275386bb0c70da98cf56487f2431668d8fd79623818d path: patches/node-fetch+2.6.7.patch - protobufjs-cli@1.1.1: - hash: 178ca99a7e71f1cce7aadae6118e2c4c3b2478d3bfb787c00a661c3b02c09923 - path: patches/protobufjs-cli+1.1.1.patch - protobufjs@7.3.2: - hash: 0ae0fcb7c2b673e67231536164cc4841642d16c8a26578de4d43637e2a6f1774 - path: patches/protobufjs+7.3.2.patch qrcode-generator@1.4.4: hash: 1f10c592d849ed4cfc9f81301196d39857b79240997ef5772138218cb3717e80 path: patches/qrcode-generator+1.4.4.patch @@ -95,6 +89,9 @@ importers: '@indutny/mac-screen-share': specifier: 1.0.13 version: 1.0.13 + '@indutny/protopiler': + specifier: 3.1.1 + version: 3.1.1 '@indutny/range-finder': specifier: 1.3.4 version: 1.3.4 @@ -141,8 +138,8 @@ importers: specifier: 2.1.2 version: 2.1.2 '@signalapp/ringrtc': - specifier: 2.66.0 - version: 2.66.0 + specifier: 2.66.1 + version: 2.66.1 '@signalapp/sqlcipher': specifier: 2.4.4 version: 2.4.4 @@ -269,9 +266,6 @@ importers: lodash: specifier: 4.17.21 version: 4.17.21 - long: - specifier: 5.2.3 - version: 5.2.3 lru-cache: specifier: 11.0.2 version: 11.0.2 @@ -313,7 +307,7 @@ importers: version: 9.8.0 protobufjs: specifier: 7.3.2 - version: 7.3.2(patch_hash=0ae0fcb7c2b673e67231536164cc4841642d16c8a26578de4d43637e2a6f1774) + version: 7.3.2 proxy-agent: specifier: 6.4.0 version: 6.4.0 @@ -442,8 +436,8 @@ importers: specifier: 0.1.61 version: 0.1.61 '@signalapp/mock-server': - specifier: 18.0.0 - version: 18.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + specifier: 18.1.0 + version: 18.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@storybook/addon-a11y': specifier: 8.4.4 version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.7.4)(utf-8-validate@5.0.10)) @@ -756,9 +750,6 @@ importers: prettier-plugin-tailwindcss: specifier: 0.7.2 version: 0.7.2(prettier@3.7.4) - protobufjs-cli: - specifier: 1.1.1 - version: 1.1.1(patch_hash=178ca99a7e71f1cce7aadae6118e2c4c3b2478d3bfb787c00a661c3b02c09923)(protobufjs@7.3.2(patch_hash=0ae0fcb7c2b673e67231536164cc4841642d16c8a26578de4d43637e2a6f1774)) react-devtools: specifier: 6.0.1 version: 6.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -1711,8 +1702,12 @@ packages: peerDependencies: prettier: ^3.0.0 - '@indutny/protopiler@2.0.2': - resolution: {integrity: sha512-AFUhU0wUcSdF1wlmK/vBlutKxFvQAwSkybB5/sQGpms17T8SfPgSrKTsPkMIX72s155V9RaygXOTgWBK2rR6pQ==} + '@indutny/protopiler@2.0.6': + resolution: {integrity: sha512-FJvX8bGpuanAXvYZkKY4cmrn+gpxx+oFJ3fyxP18WHhC6jLUQ8KuWMfxaZU/K+1IBQeRbh67wtC0iAshX0Tnjw==} + hasBin: true + + '@indutny/protopiler@3.1.1': + resolution: {integrity: sha512-EOlIbOOSOiQrkXGNS2mRZRr6mOueYBF1F/Q4oPNn0lK0Cd01R4T0mfrPUysXALnmcNn/D0AENmx+g4xG5W7keQ==} hasBin: true '@indutny/range-finder@1.3.4': @@ -1743,8 +1738,8 @@ packages: '@internationalized/string@3.2.5': resolution: {integrity: sha512-rKs71Zvl2OKOHM+mzAFMIyqR5hI1d1O6BBkMK2/lkfg3fkmVh9Eeg0awcA8W2WqYqDOv6a86DIOlFpggwLtbuw==} - '@ioredis/commands@1.2.0': - resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} @@ -1864,10 +1859,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@jsdoc/salty@0.2.9': - resolution: {integrity: sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==} - engines: {node: '>=v12.0.0'} - '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -3474,8 +3465,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - '@redis/client@1.6.0': - resolution: {integrity: sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==} + '@redis/client@1.6.1': + resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} engines: {node: '>=14'} '@rtsao/scc@1.1.0': @@ -3499,8 +3490,8 @@ packages: '@signalapp/minimask@1.0.1': resolution: {integrity: sha512-QAwo0joA60urTNbW9RIz6vLKQjy+jdVtH7cvY0wD9PVooD46MAjE40MLssp4xUJrph91n2XvtJ3pbEUDrmT2AA==} - '@signalapp/mock-server@18.0.0': - resolution: {integrity: sha512-YNmFyzBCW/nBjPBX6va5TPWnbg3A7euUHHUTk9wmDoAJ4A1gv+whDZGCfVmqI7l4Mhn9l4p5JSGkGUCpgUT/Pg==} + '@signalapp/mock-server@18.1.0': + resolution: {integrity: sha512-kM8zkjmWjf3vvX/rEsBwZP/K/oX1gXBxAb4eudsxks6zBg++5aNftVljw50gVMMJaaawNdHg0pDHp+3t6wzpzA==} '@signalapp/parchment-cjs@3.0.1': resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==} @@ -3509,8 +3500,8 @@ packages: resolution: {integrity: sha512-y2sgqdivlrG41J4Zvt/82xtH/PZjDlgItqlD2g/Cv3ZbjlR6cGhTNXbfNygCJB8nXj+C7I28pjt1Zm3k0pv2mg==} engines: {npm: '>=8.2.3'} - '@signalapp/ringrtc@2.66.0': - resolution: {integrity: sha512-BUUVgdfldAWMsoGnxcU7zTNEju0BdTa0S+fjCa5ZMTaLPjC+9BGfNOxDIKjzVoQhk15yUJ8O3MtYdkMdwQQwyQ==} + '@signalapp/ringrtc@2.66.1': + resolution: {integrity: sha512-Vf5xxZRq4y//3PvZ1gUo3k/ge4RSJ5QqInCGUYDmFmqUVDv3bVTX22pcFkUaKAFzN6zKFe5lVxr3GCGG+Ay2Fg==} hasBin: true '@signalapp/sqlcipher@2.4.4': @@ -4115,15 +4106,9 @@ packages: '@types/lodash@4.14.106': resolution: {integrity: sha512-tOSvCVrvSqFZ4A/qrqqm6p37GZoawsZtoR0SJhlF7EonNZUgrn8FfT+RNQ11h+NUpMt6QVe36033f3qEKBwfWA==} - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - '@types/mdast@3.0.15': resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/memoizee@0.4.11': resolution: {integrity: sha512-2gyorIBZu8GoDr9pYjROkxWWcFtHCquF7TVbN2I+/OvgZhnIGQS0vX5KJz4lXNKb8XOSfxFOSG5OLru1ESqLUg==} @@ -4874,9 +4859,6 @@ packages: blob-util@2.0.2: resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - blueimp-load-image@5.16.0: resolution: {integrity: sha512-3DUSVdOtlfNRk7moRZuTwDmA3NnG8KIJuLcq3c0J7/BIr6X3Vb/EpX3kUH1joxUhmoVF4uCpDfz7wHkz8pQajA==} @@ -5035,10 +5017,6 @@ packages: casual@1.6.2: resolution: {integrity: sha512-NQObL800rg32KZ9bBajHbyDjxLXxxuShChQg7A4tbSeG3n1t7VYGOSkzFSI9gkSgOHp+xilEJ7G0L5l6M30KYA==} - catharsis@0.9.0: - resolution: {integrity: sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==} - engines: {node: '>= 10'} - ccount@1.1.0: resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} @@ -6027,11 +6005,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escodegen@1.14.3: - resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} - engines: {node: '>=4.0'} - hasBin: true - escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -7019,8 +6992,8 @@ packages: intl-tel-input@24.7.0: resolution: {integrity: sha512-OjkhKen4SJUI2kN9OHpb8ReNN619sB9gECPq51dn3zKEWvif3mnSjmrtWhm8ABIb7Ijs+AAYSS5sI33Sb4YqvQ==} - ioredis@5.6.0: - resolution: {integrity: sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==} + ioredis@5.10.0: + resolution: {integrity: sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==} engines: {node: '>=12.22.0'} ip-address@9.0.5: @@ -7570,9 +7543,6 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - js2xmlparser@4.0.2: - resolution: {integrity: sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==} - jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} @@ -7580,11 +7550,6 @@ packages: resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} engines: {node: '>=12.0.0'} - jsdoc@4.0.4: - resolution: {integrity: sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==} - engines: {node: '>=12.0.0'} - hasBin: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -7661,9 +7626,6 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - klaw@3.0.0: - resolution: {integrity: sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==} - kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -7695,10 +7657,6 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} - levn@0.3.0: - resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} - engines: {node: '>= 0.8.0'} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -7975,24 +7933,9 @@ packages: map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} - markdown-it-anchor@8.6.7: - resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} - peerDependencies: - '@types/markdown-it': '*' - markdown-it: '*' - - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - markdown-table@2.0.0: resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} - marked@4.3.0: - resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} - engines: {node: '>= 12'} - hasBin: true - matcher@3.0.0: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} @@ -8046,9 +7989,6 @@ packages: mdn-data@2.21.0: resolution: {integrity: sha512-+ZKPQezM5vYJIkCxaC+4DTnRrVZR1CgsKLu5zsQERQx6Tea8Y+wMx5A24rq8A8NepCeatIQufVAekKNgiBMsGQ==} - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -8520,10 +8460,6 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true - optionator@0.8.3: - resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} - engines: {node: '>= 0.8.0'} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -8934,10 +8870,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - prelude-ls@1.1.2: - resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} - engines: {node: '>= 0.8.0'} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -9075,13 +9007,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - protobufjs-cli@1.1.1: - resolution: {integrity: sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==} - engines: {node: '>=12.0.0'} - hasBin: true - peerDependencies: - protobufjs: ^7.0.0 - protobufjs@7.3.2: resolution: {integrity: sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==} engines: {node: '>=12.0.0'} @@ -9106,10 +9031,6 @@ packages: pumpify@2.0.1: resolution: {integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==} - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -9458,9 +9379,6 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - requizzle@0.2.4: - resolution: {integrity: sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==} - resedit@1.7.2: resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} engines: {node: '>=12', npm: '>=6'} @@ -10114,12 +10032,12 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} @@ -10310,10 +10228,6 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - type-check@0.3.2: - resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} - engines: {node: '>= 0.8.0'} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -10384,11 +10298,6 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -10397,9 +10306,6 @@ packages: resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} engines: {node: '>=0.10.0'} - underscore@1.13.7: - resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} - undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} @@ -10847,9 +10753,6 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} - xmlcreate@2.0.4: - resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==} - xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -11860,7 +11763,9 @@ snapshots: prettier: 3.7.4 rxjs: 6.6.7 - '@indutny/protopiler@2.0.2': {} + '@indutny/protopiler@2.0.6': {} + + '@indutny/protopiler@3.1.1': {} '@indutny/range-finder@1.3.4': dependencies: @@ -11903,7 +11808,7 @@ snapshots: dependencies: '@swc/helpers': 0.5.15 - '@ioredis/commands@1.2.0': + '@ioredis/commands@1.5.1': optional: true '@isaacs/balanced-match@4.0.1': {} @@ -12128,10 +12033,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@jsdoc/salty@0.2.9': - dependencies: - lodash: 4.17.21 - '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -14256,7 +14157,7 @@ snapshots: '@react-types/shared': 3.27.0(react@18.3.1) react: 18.3.1 - '@redis/client@1.6.0': + '@redis/client@1.6.1': dependencies: cluster-key-slot: 1.1.2 generic-pool: 3.9.0 @@ -14287,10 +14188,10 @@ snapshots: '@signalapp/minimask@1.0.1': {} - '@signalapp/mock-server@18.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + '@signalapp/mock-server@18.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@indutny/parallel-prettier': 3.0.0(prettier@3.7.4) - '@indutny/protopiler': 2.0.2 + '@indutny/protopiler': 2.0.6 '@signalapp/libsignal-client': 0.76.7 '@tus/file-store': 1.5.1 '@tus/server': 1.10.2 @@ -14318,7 +14219,7 @@ snapshots: lodash: 4.17.21 quill-delta: 5.1.0 - '@signalapp/ringrtc@2.66.0': + '@signalapp/ringrtc@2.66.1': dependencies: https-proxy-agent: 7.0.6 tar: 7.5.2 @@ -14792,7 +14693,7 @@ snapshots: '@tailwindcss/oxide@4.1.7': dependencies: detect-libc: 2.0.4 - tar: 7.4.3 + tar: 7.5.2 optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.7 '@tailwindcss/oxide-darwin-arm64': 4.1.7 @@ -14867,7 +14768,7 @@ snapshots: '@tus/utils': 0.5.1 debug: 4.3.7(supports-color@8.1.1) optionalDependencies: - '@redis/client': 1.6.0 + '@redis/client': 1.6.1 transitivePeerDependencies: - supports-color @@ -14877,8 +14778,8 @@ snapshots: debug: 4.3.7(supports-color@8.1.1) lodash.throttle: 4.1.1 optionalDependencies: - '@redis/client': 1.6.0 - ioredis: 5.6.0 + '@redis/client': 1.6.1 + ioredis: 5.10.0 transitivePeerDependencies: - supports-color @@ -15063,17 +14964,10 @@ snapshots: '@types/lodash@4.14.106': {} - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - '@types/mdast@3.0.15': dependencies: '@types/unist': 2.0.11 - '@types/mdurl@2.0.0': {} - '@types/memoizee@0.4.11': {} '@types/mime@1.3.5': {} @@ -15983,8 +15877,6 @@ snapshots: blob-util@2.0.2: {} - bluebird@3.7.2: {} - blueimp-load-image@5.16.0: {} blurhash@2.0.5: {} @@ -16219,10 +16111,6 @@ snapshots: mersenne-twister: 1.1.0 moment: 2.30.1 - catharsis@0.9.0: - dependencies: - lodash: 4.17.21 - ccount@1.1.0: {} chai-as-promised@7.1.1(chai@4.4.1): @@ -17419,15 +17307,6 @@ snapshots: escape-string-regexp@4.0.0: {} - escodegen@1.14.3: - dependencies: - esprima: 4.0.1 - estraverse: 4.3.0 - esutils: 2.0.3 - optionator: 0.8.3 - optionalDependencies: - source-map: 0.6.1 - escodegen@2.1.0: dependencies: esprima: 4.0.1 @@ -18709,9 +18588,9 @@ snapshots: intl-tel-input@24.7.0: {} - ioredis@5.6.0: + ioredis@5.10.0: dependencies: - '@ioredis/commands': 1.2.0 + '@ioredis/commands': 1.5.1 cluster-key-slot: 1.1.2 debug: 4.3.7(supports-color@8.1.1) denque: 2.1.0 @@ -19452,32 +19331,10 @@ snapshots: dependencies: argparse: 2.0.1 - js2xmlparser@4.0.2: - dependencies: - xmlcreate: 2.0.4 - jsbn@1.1.0: {} jsdoc-type-pratt-parser@4.1.0: {} - jsdoc@4.0.4: - dependencies: - '@babel/parser': 7.26.8 - '@jsdoc/salty': 0.2.9 - '@types/markdown-it': 14.1.2 - bluebird: 3.7.2 - catharsis: 0.9.0 - escape-string-regexp: 2.0.0 - js2xmlparser: 4.0.2 - klaw: 3.0.0 - markdown-it: 14.1.0 - markdown-it-anchor: 8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.0) - marked: 4.3.0 - mkdirp: 1.0.4 - requizzle: 0.2.4 - strip-json-comments: 3.1.1 - underscore: 1.13.7 - jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -19559,10 +19416,6 @@ snapshots: kind-of@6.0.3: {} - klaw@3.0.0: - dependencies: - graceful-fs: 4.2.11 - kleur@3.0.3: {} known-css-properties@0.34.0: {} @@ -19588,11 +19441,6 @@ snapshots: leven@3.1.0: {} - levn@0.3.0: - dependencies: - prelude-ls: 1.1.2 - type-check: 0.3.2 - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -19847,26 +19695,10 @@ snapshots: map-or-similar@1.5.0: {} - markdown-it-anchor@8.6.7(@types/markdown-it@14.1.2)(markdown-it@14.1.0): - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - - markdown-it@14.1.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - markdown-table@2.0.0: dependencies: repeat-string: 1.6.1 - marked@4.3.0: {} - matcher@3.0.0: dependencies: escape-string-regexp: 4.0.0 @@ -19944,8 +19776,6 @@ snapshots: mdn-data@2.21.0: {} - mdurl@2.0.0: {} - media-typer@0.3.0: {} memfs-or-file-map-to-github-branch@1.2.1(encoding@0.1.13): @@ -20460,15 +20290,6 @@ snapshots: opener@1.5.2: {} - optionator@0.8.3: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.3.0 - prelude-ls: 1.1.2 - type-check: 0.3.2 - word-wrap: 1.2.5 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -20870,8 +20691,6 @@ snapshots: commander: 9.5.0 optional: true - prelude-ls@1.1.2: {} - prelude-ls@1.2.1: {} prepend-http@1.0.4: {} @@ -20944,21 +20763,7 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - protobufjs-cli@1.1.1(patch_hash=178ca99a7e71f1cce7aadae6118e2c4c3b2478d3bfb787c00a661c3b02c09923)(protobufjs@7.3.2(patch_hash=0ae0fcb7c2b673e67231536164cc4841642d16c8a26578de4d43637e2a6f1774)): - dependencies: - chalk: 4.1.2 - escodegen: 1.14.3 - espree: 9.6.1 - estraverse: 5.3.0 - glob: 8.1.0 - jsdoc: 4.0.4 - minimist: 1.2.8 - protobufjs: 7.3.2(patch_hash=0ae0fcb7c2b673e67231536164cc4841642d16c8a26578de4d43637e2a6f1774) - semver: 7.6.3 - tmp: 0.2.3 - uglify-js: 3.19.3 - - protobufjs@7.3.2(patch_hash=0ae0fcb7c2b673e67231536164cc4841642d16c8a26578de4d43637e2a6f1774): + protobufjs@7.3.2: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -21006,8 +20811,6 @@ snapshots: inherits: 2.0.4 pump: 3.0.3 - punycode.js@2.3.1: {} - punycode@1.4.1: {} punycode@2.3.1: {} @@ -21550,10 +21353,6 @@ snapshots: requires-port@1.0.0: {} - requizzle@0.2.4: - dependencies: - lodash: 4.17.21 - resedit@1.7.2: dependencies: pe-library: 0.4.1 @@ -22543,10 +22342,6 @@ snapshots: tslib: 1.14.1 typescript: 5.6.3 - type-check@0.3.2: - dependencies: - prelude-ls: 1.1.2 - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -22616,8 +22411,6 @@ snapshots: uc.micro@2.1.0: {} - uglify-js@3.19.3: {} - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.3 @@ -22627,8 +22420,6 @@ snapshots: unc-path-regex@0.1.2: {} - underscore@1.13.7: {} - undici-types@6.19.8: {} undici-types@6.20.0: {} @@ -23172,8 +22963,6 @@ snapshots: xmlbuilder@15.1.1: {} - xmlcreate@2.0.4: {} - xtend@4.0.2: {} y18n@4.0.3: {} diff --git a/protos/ArtCreator.proto b/protos/ArtCreator.proto deleted file mode 100644 index 378bd38eee..0000000000 --- a/protos/ArtCreator.proto +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -package signalservice; - -message ArtProvisioningEnvelope { - optional bytes publicKey = 1; - optional bytes ciphertext = 2; -} - -message ArtProvisioningMessage { - optional string username = 1; - optional string password = 2; -} diff --git a/protos/ContactDiscovery.proto b/protos/ContactDiscovery.proto deleted file mode 100644 index d1f5e046b9..0000000000 --- a/protos/ContactDiscovery.proto +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -package signalservice; - -message CDSClientRequest { - // Each ACI/UAK pair is a 32-byte buffer, containing the 16-byte ACI followed - // by its 16-byte UAK. - optional bytes aci_uak_pairs = 1; - - // Each E164 is an 8-byte big-endian number, as 8 bytes. - optional bytes prev_e164s = 2; - optional bytes new_e164s = 3; - optional bytes discard_e164s = 4; - - // If true, the client has more pairs or e164s to send. If false or unset, - // this is the client's last request, and processing should commence. - optional bool has_more = 5; - - // If set, a token which allows rate limiting to discount the e164s in - // the request's prev_e164s, only counting new_e164s. If not set, then - // rate limiting considers both prev_e164s' and new_e164s' size. - optional bytes token = 6; - - // After receiving a new token from the server, send back a message just - // containing a token_ack. - optional bool token_ack = 7; - - // Request that, if the server allows, both ACI and PNI be returned even - // if the aci_uak_pairs don't match. - optional bool return_acis_without_uaks = 8; -} - -message CDSClientResponse { - // Each triple is an 8-byte e164, a 16-byte PNI, and a 16-byte ACI. - // If the e164 was not found, PNI and ACI are all zeros. If the PNI - // was found but the ACI was not, the PNI will be non-zero and the ACI - // will be all zeros. ACI will be returned if one of the returned - // PNIs has an ACI/UAK pair that matches. - // - // Should the request be successful (IE: a successful status returned), - // |e164_pni_aci_triple| will always equal |e164| of the request, - // so the entire marshalled size of the response will be (2+32)*|e164|, - // where the additional 2 bytes are the id/type/length additions of the - // protobuf marshaling added to each byte array. This avoids any data - // leakage based on the size of the encrypted output. - optional bytes e164_pni_aci_triples = 1; - - // A token which allows subsequent calls' rate limiting to discount the - // e164s sent up in this request, only counting those in the next - // request's new_e164s. - optional bytes token = 3; -} diff --git a/protos/SignalService.proto b/protos/SignalService.proto index e43df52531..d8e011cb12 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -104,17 +104,20 @@ message Envelope { } message Content { - optional DataMessage dataMessage = 1; - optional SyncMessage syncMessage = 2; - optional CallMessage callMessage = 3; - optional NullMessage nullMessage = 4; - optional ReceiptMessage receiptMessage = 5; - optional TypingMessage typingMessage = 6; + oneof content { + DataMessage dataMessage = 1; + SyncMessage syncMessage = 2; + CallMessage callMessage = 3; + NullMessage nullMessage = 4; + ReceiptMessage receiptMessage = 5; + TypingMessage typingMessage = 6; + bytes /* DecryptionErrorMessage */ decryptionErrorMessage = 8; + StoryMessage storyMessage = 9; + EditMessage editMessage = 11; + } + optional bytes /* SenderKeyDistributionMessage */ senderKeyDistributionMessage = 7; - optional bytes /* DecryptionErrorMessage */ decryptionErrorMessage = 8; - optional StoryMessage storyMessage = 9; optional PniSignatureMessage pniSignatureMessage = 10; - optional EditMessage editMessage = 11; } message CallMessage { @@ -237,6 +240,7 @@ message DataMessage { enum Type { NORMAL = 0; GIFT_BADGE = 1; + POLL = 2; } message QuotedAttachment { @@ -496,6 +500,12 @@ message TextAttachment { } message Gradient { + // Color ordering: + // 0 degrees: bottom-to-top + // 90 degrees: left-to-right + // 180 degrees: top-to-bottom + // 270 degrees: right-to-left + optional uint32 startColor = 1; // deprecated: this field will be removed in a future release. optional uint32 endColor = 2; // deprecated: this field will be removed in a future release. optional uint32 angle = 3; // degrees @@ -608,7 +618,7 @@ message SyncMessage { optional bool unidentifiedDeliveryIndicators = 2; optional bool typingIndicators = 3; reserved /* linkPreviews */ 4; - optional uint32 provisioningVersion = 5; + reserved /* provisioningVersion */ 5; optional bool linkPreviews = 6; } @@ -723,8 +733,8 @@ message SyncMessage { /* Data identifying a conversation. The service ID for 1:1, the group ID for * group, or the room ID for an ad-hoc call. See also - * `CallLogEvent/peerId`. */ - optional bytes peerId = 1; + * `CallLogEvent/conversationId`. */ + optional bytes conversationId = 1; /* An identifier for a call. Generated directly for 1:1, or derived from * the era ID for group and ad-hoc calls. See also `CallLogEvent/callId`. */ optional uint64 callId = 2; @@ -743,7 +753,7 @@ message SyncMessage { optional bytes rootKey = 1; optional bytes adminPasskey = 2; optional Type type = 3; // defaults to UPDATE - reserved 4; // was epoch, never used + reserved /*epoch*/ 4; } message CallLogEvent { @@ -758,8 +768,8 @@ message SyncMessage { optional uint64 timestamp = 2; /* Data identifying a conversation. The service ID for 1:1, the group ID for * group, or the room ID for an ad-hoc call. See also - * `CallEvent/peerId`. */ - optional bytes peerId = 3; + * `CallEvent/conversationId`. */ + optional bytes conversationId = 3; /* An identifier for a call. Generated directly for 1:1, or derived from * the era ID for group and ad-hoc calls. See also `CallEvent/callId`. */ optional uint64 callId = 4; @@ -840,31 +850,40 @@ message SyncMessage { } } - optional Sent sent = 1; - optional Contacts contacts = 2; + oneof content { + Sent sent = 1; + Contacts contacts = 2; + Request request = 4; + Blocked blocked = 6; + Verified verified = 7; + Configuration configuration = 9; + ViewOnceOpen viewOnceOpen = 11; + FetchLatest fetchLatest = 12; + Keys keys = 13; + MessageRequestResponse messageRequestResponse = 14; + OutgoingPayment outgoingPayment = 15; + PniChangeNumber pniChangeNumber = 18; + CallEvent callEvent = 19; + CallLinkUpdate callLinkUpdate = 20; + CallLogEvent callLogEvent = 21; + DeleteForMe deleteForMe = 22; + DeviceNameChange deviceNameChange = 23; + AttachmentBackfillRequest attachmentBackfillRequest = 24; + AttachmentBackfillResponse attachmentBackfillResponse = 25; + } + reserved /*groups*/ 3; - optional Request request = 4; + + // Protobufs don't allow `repeated` fields to be inside of `oneof` so while + // the fields below are mutually exclusive with the rest of the values above + // we have to place them outside of `oneof`. repeated Read read = 5; - optional Blocked blocked = 6; - optional Verified verified = 7; - optional Configuration configuration = 9; - optional bytes padding = 8; repeated StickerPackOperation stickerPackOperation = 10; - optional ViewOnceOpen viewOnceOpen = 11; - optional FetchLatest fetchLatest = 12; - optional Keys keys = 13; - optional MessageRequestResponse messageRequestResponse = 14; - optional OutgoingPayment outgoingPayment = 15; repeated Viewed viewed = 16; + reserved /*pniIdentity*/ 17; - optional PniChangeNumber pniChangeNumber = 18; - optional CallEvent callEvent = 19; - optional CallLinkUpdate callLinkUpdate = 20; - optional CallLogEvent callLogEvent = 21; - optional DeleteForMe deleteForMe = 22; - optional DeviceNameChange deviceNameChange = 23; - optional AttachmentBackfillRequest attachmentBackfillRequest = 24; - optional AttachmentBackfillResponse attachmentBackfillResponse = 25; + + optional bytes padding = 8; } message AttachmentPointer { diff --git a/protos/SubProtocol.proto b/protos/SubProtocol.proto deleted file mode 100644 index f416073269..0000000000 --- a/protos/SubProtocol.proto +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2014 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -package signalservice; - -option java_package = "org.whispersystems.websocket.messages.protobuf"; - -message WebSocketRequestMessage { - optional string verb = 1; - optional string path = 2; - optional bytes body = 3; - repeated string headers = 5; - optional uint64 id = 4; -} - -message WebSocketResponseMessage { - optional uint64 id = 1; - optional uint32 status = 2; - optional string message = 3; - repeated string headers = 5; - optional bytes body = 4; -} - -message WebSocketMessage { - enum Type { - UNKNOWN = 0; - REQUEST = 1; - RESPONSE = 2; - } - - optional Type type = 1; - optional WebSocketRequestMessage request = 2; - optional WebSocketResponseMessage response = 3; -} diff --git a/ts/Crypto.node.ts b/ts/Crypto.node.ts index 67ee241ce6..78f2735fe2 100644 --- a/ts/Crypto.node.ts +++ b/ts/Crypto.node.ts @@ -1,7 +1,6 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import Long from 'long'; import lodash from 'lodash'; import { Aci, Pni, hkdf } from '@signalapp/libsignal-client'; import type { PublicKey, PrivateKey } from '@signalapp/libsignal-client'; @@ -223,7 +222,7 @@ export function deriveStorageServiceKey(masterKey: Uint8Array): Uint8Array { export function deriveStorageManifestKey( storageServiceKey: Uint8Array, - version: Long = Long.fromNumber(0) + version = 0n ): Uint8Array { return hmacSha256(storageServiceKey, Bytes.fromString(`Manifest_${version}`)); } diff --git a/ts/SignalProtocolStore.preload.ts b/ts/SignalProtocolStore.preload.ts index 715f7aa833..2a0ff4cf82 100644 --- a/ts/SignalProtocolStore.preload.ts +++ b/ts/SignalProtocolStore.preload.ts @@ -2105,6 +2105,7 @@ export class SignalProtocolStore extends EventEmitter { nonblockingApproval = false; } + strictAssert(publicKey.length > 0, 'Empty public key'); return this.#_runOnIdentityQueue( encodedAddress.serviceId, zone, diff --git a/ts/groups.preload.ts b/ts/groups.preload.ts index 8a073f5892..5de6ca3db1 100644 --- a/ts/groups.preload.ts +++ b/ts/groups.preload.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import lodash from 'lodash'; -import Long from 'long'; import type { ClientZkGroupCipher } from '@signalapp/libsignal-client/zkgroup.js'; import { LRUCache } from 'lru-cache'; import { createLogger } from './logging/log.std.js'; @@ -133,14 +132,17 @@ import { SERVER_STRING_BYTE_LIMIT, } from './types/GroupMemberLabels.std.js'; import { getConversationIdForLogging } from './util/idForLogging.preload.js'; +import { toNumber } from './util/toNumber.std.js'; + +import Actions = Proto.GroupChange.Actions; +import AccessRequired = Proto.AccessControl.AccessRequired; +import MemberRole = Proto.Member.Role; const { compact, difference, flatten, fromPairs, isNumber, omit, values } = lodash; const log = createLogger('groups'); -type AccessRequiredEnum = Proto.AccessControl.AccessRequired; - export { joinViaLink } from './groups/joinViaLink.preload.js'; export type GroupFields = { @@ -163,6 +165,41 @@ if (!isNumber(MAX_MESSAGE_SCHEMA)) { ); } +function toGroupActionsParams( + input: Pick & + Partial> +): Actions.Params { + return { + sourceUserId: null, + groupId: null, + addMembers: null, + deleteMembers: null, + modifyMemberRoles: null, + modifyMemberLabels: null, + modifyMemberProfileKeys: null, + addMembersPendingProfileKey: null, + deleteMembersPendingProfileKey: null, + promoteMembersPendingProfileKey: null, + modifyTitle: null, + modifyAvatar: null, + modifyDisappearingMessageTimer: null, + modifyAttributesAccess: null, + modifyMemberAccess: null, + modifyAddFromInviteLinkAccess: null, + modifyMemberLabelAccess: null, + addMembersPendingAdminApproval: null, + deleteMembersPendingAdminApproval: null, + promoteMembersPendingAdminApproval: null, + modifyInviteLinkPassword: null, + modifyDescription: null, + modifyAnnouncementsOnly: null, + addMembersBanned: null, + deleteMembersBanned: null, + promoteMembersPendingPniAciProfileKey: null, + ...input, + }; +} + type UpdatesResultType = { // The array of new messages to be added into the message timeline groupChangeMessages: Array; @@ -259,11 +296,13 @@ export function buildGroupLink( strictAssert(masterKey, 'buildGroupLink requires the master key!'); const bytes = Proto.GroupInviteLink.encode({ - contentsV1: { - groupMasterKey: Bytes.fromBase64(masterKey), - inviteLinkPassword: Bytes.fromBase64(groupInviteLinkPassword), + contents: { + contentsV1: { + groupMasterKey: Bytes.fromBase64(masterKey), + inviteLinkPassword: Bytes.fromBase64(groupInviteLinkPassword), + }, }, - }).finish(); + }); const inviteCode = toWebSafeBase64(Bytes.toBase64(bytes)); @@ -278,10 +317,7 @@ export function parseGroupLink(value: string): { const buffer = Bytes.fromBase64(base64); const inviteLinkProto = Proto.GroupInviteLink.decode(buffer); - if ( - inviteLinkProto.contents !== 'contentsV1' || - !inviteLinkProto.contentsV1 - ) { + if (!inviteLinkProto.contents?.contentsV1) { const error = new Error( 'parseGroupLink: Parsed proto is missing contentsV1' ); @@ -292,7 +328,7 @@ export function parseGroupLink(value: string): { const { groupMasterKey: groupMasterKeyRaw, inviteLinkPassword: inviteLinkPasswordRaw, - } = inviteLinkProto.contentsV1; + } = inviteLinkProto.contents.contentsV1; if (!groupMasterKeyRaw || !groupMasterKeyRaw.length) { throw new Error('contentsV1.groupMasterKey had no data!'); @@ -331,8 +367,10 @@ async function uploadAvatar(options: { const hash = computeHash(data); const blobPlaintext = Proto.GroupAttributeBlob.encode({ - avatar: data, - }).finish(); + content: { + avatar: data, + }, + }); const ciphertext = encryptGroupBlob(clientZkGroupCipher, blobPlaintext); const key = await makeRequestWithCredentials({ @@ -361,8 +399,10 @@ function buildGroupTitleBuffer( title: string ): Uint8Array { const titleBlobPlaintext = Proto.GroupAttributeBlob.encode({ - title, - }).finish(); + content: { + title, + }, + }); const result = encryptGroupBlob(clientZkGroupCipher, titleBlobPlaintext); @@ -378,8 +418,10 @@ function buildGroupDescriptionBuffer( description: string ): Uint8Array { const attrsBlobPlaintext = Proto.GroupAttributeBlob.encode({ - descriptionText: description, - }).finish(); + content: { + descriptionText: description, + }, + }); const result = encryptGroupBlob(clientZkGroupCipher, attrsBlobPlaintext); @@ -407,9 +449,7 @@ function buildGroupProto( > & { avatarUrl?: string; } -): Proto.Group { - const MEMBER_ROLE_ENUM = Proto.Member.Role; - const ACCESS_ENUM = Proto.AccessControl.AccessRequired; +): Proto.Group.Params { const logId = `groupv2(${attributes.id})`; const { publicParams, secretParams } = attributes; @@ -430,76 +470,83 @@ function buildGroupProto( const clientZkProfileCipher = getClientZkProfileOperations( serverPublicParamsBase64 ); - const proto = new Proto.Group(); - proto.publicKey = Bytes.fromBase64(publicParams); - proto.version = attributes.revision || 0; + const publicKey = Bytes.fromBase64(publicParams); + const version = attributes.revision || 0; + let title: Uint8Array | null = null; if (attributes.name) { - proto.title = buildGroupTitleBuffer(clientZkGroupCipher, attributes.name); + title = buildGroupTitleBuffer(clientZkGroupCipher, attributes.name); } + let avatarUrl: string | null = null; if (attributes.avatarUrl) { - proto.avatarUrl = attributes.avatarUrl; + avatarUrl = attributes.avatarUrl; } + let disappearingMessagesTimer: Uint8Array | null = null; if (attributes.expireTimer) { const timerBlobPlaintext = Proto.GroupAttributeBlob.encode({ - disappearingMessagesDuration: attributes.expireTimer, - }).finish(); - proto.disappearingMessagesTimer = encryptGroupBlob( + content: { + disappearingMessagesDuration: attributes.expireTimer, + }, + }); + disappearingMessagesTimer = encryptGroupBlob( clientZkGroupCipher, timerBlobPlaintext ); } - const accessControl = new Proto.AccessControl(); - if (attributes.accessControl) { - accessControl.attributes = - attributes.accessControl.attributes || ACCESS_ENUM.MEMBER; - accessControl.members = - attributes.accessControl.members || ACCESS_ENUM.MEMBER; - } else { - accessControl.attributes = ACCESS_ENUM.MEMBER; - accessControl.members = ACCESS_ENUM.MEMBER; - } - proto.accessControl = accessControl; + const accessControl: Proto.AccessControl.Params = { + attributes: attributes.accessControl?.attributes ?? AccessRequired.MEMBER, + members: attributes.accessControl?.members ?? AccessRequired.MEMBER, + addFromInviteLink: + attributes.accessControl?.addFromInviteLink ?? AccessRequired.MEMBER, + memberLabel: attributes.accessControl?.memberLabel ?? AccessRequired.MEMBER, + }; - proto.members = (attributes.membersV2 || []).map(item => { - const member = new Proto.Member(); + const members = (attributes.membersV2 || []).map( + (item): Proto.Member.Params => { + const conversation = window.ConversationController.get(item.aci); + if (!conversation) { + throw new Error( + `buildGroupProto/${logId}: no conversation for member!` + ); + } - const conversation = window.ConversationController.get(item.aci); - if (!conversation) { - throw new Error(`buildGroupProto/${logId}: no conversation for member!`); - } - - const profileKeyCredentialBase64 = conversation.get('profileKeyCredential'); - if (!profileKeyCredentialBase64) { - throw new Error( - `buildGroupProto/${logId}: member was missing profileKeyCredential!` + const profileKeyCredentialBase64 = conversation.get( + 'profileKeyCredential' ); + if (!profileKeyCredentialBase64) { + throw new Error( + `buildGroupProto/${logId}: member was missing profileKeyCredential!` + ); + } + const presentation = createProfileKeyCredentialPresentation( + clientZkProfileCipher, + profileKeyCredentialBase64, + secretParams + ); + + return { + role: item.role || MemberRole.DEFAULT, + presentation, + // intentionally left null + userId: null, + profileKey: null, + joinedAtVersion: null, + labelEmoji: null, + labelString: null, + }; } - const presentation = createProfileKeyCredentialPresentation( - clientZkProfileCipher, - profileKeyCredentialBase64, - secretParams - ); - - member.role = item.role || MEMBER_ROLE_ENUM.DEFAULT; - member.presentation = presentation; - - return member; - }); + ); const ourAci = itemStorage.user.getCheckedAci(); const ourAciCipherTextBuffer = encryptServiceId(clientZkGroupCipher, ourAci); - proto.membersPendingProfileKey = (attributes.pendingMembersV2 || []).map( - item => { - const pendingMember = new Proto.MemberPendingProfileKey(); - const member = new Proto.Member(); - + const membersPendingProfileKey = (attributes.pendingMembersV2 || []).map( + (item): Proto.MemberPendingProfileKey.Params => { const conversation = window.ConversationController.get(item.serviceId); if (!conversation) { throw new Error('buildGroupProto: no conversation for pending member!'); @@ -513,18 +560,41 @@ function buildGroupProto( clientZkGroupCipher, serviceId ); - member.userId = uuidCipherTextBuffer; - member.role = item.role || MEMBER_ROLE_ENUM.DEFAULT; - pendingMember.member = member; - pendingMember.timestamp = Long.fromNumber(item.timestamp); - pendingMember.addedByUserId = ourAciCipherTextBuffer; - - return pendingMember; + return { + timestamp: BigInt(item.timestamp), + addedByUserId: ourAciCipherTextBuffer, + member: { + userId: uuidCipherTextBuffer, + role: item.role || MemberRole.DEFAULT, + // intentionally left null + presentation: null, + profileKey: null, + joinedAtVersion: null, + labelEmoji: null, + labelString: null, + }, + }; } ); - return proto; + return { + publicKey, + version, + title, + avatarUrl, + disappearingMessagesTimer, + accessControl, + members, + membersPendingProfileKey, + + // Can't create group with these initial fields + description: null, + membersPendingAdminApproval: null, + membersBanned: null, + inviteLinkPassword: null, + announcementsOnly: null, + }; } export async function buildAddMembersChange( @@ -533,9 +603,7 @@ export async function buildAddMembersChange( 'bannedMembersV2' | 'id' | 'publicParams' | 'revision' | 'secretParams' >, conversationIds: ReadonlyArray -): Promise { - const MEMBER_ROLE_ENUM = Proto.Member.Role; - +): Promise { const { id, publicParams, revision, secretParams } = conversation; const logId = `groupv2(${id})`; @@ -563,10 +631,10 @@ export async function buildAddMembersChange( const now = Date.now(); - const addMembers: Array = []; - const addMembersPendingProfileKey: Array = + const addMembers: Array = []; + const addMembersPendingProfileKey: Array = []; - const actions = new Proto.GroupChange.Actions(); + const deleteMembersBanned: Array = []; await Promise.all( conversationIds.map(async conversationId => { @@ -596,37 +664,37 @@ export async function buildAddMembersChange( const profileKey = contact.get('profileKey'); const profileKeyCredential = contact.get('profileKeyCredential'); - const member = new Proto.Member(); - member.userId = encryptServiceId(clientZkGroupCipher, serviceId); - member.role = MEMBER_ROLE_ENUM.DEFAULT; - member.joinedAtVersion = newGroupVersion; + const member = { + userId: encryptServiceId(clientZkGroupCipher, serviceId), + role: MemberRole.DEFAULT, + joinedAtVersion: newGroupVersion, + profileKey: null, + labelEmoji: null, + labelString: null, + } satisfies Partial; // This is inspired by [Android's equivalent code][0]. // // [0]: https://github.com/signalapp/Signal-Android/blob/2be306867539ab1526f0e49d1aa7bd61e783d23f/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java#L152-L174 if (profileKey && profileKeyCredential) { - member.presentation = createProfileKeyCredentialPresentation( + const presentation = createProfileKeyCredentialPresentation( clientZkProfileCipher, profileKeyCredential, secretParams ); - const addMemberAction = new Proto.GroupChange.Actions.AddMemberAction(); - addMemberAction.added = member; - addMemberAction.joinFromInviteLink = false; - - addMembers.push(addMemberAction); + addMembers.push({ + added: { ...member, presentation }, + joinFromInviteLink: false, + }); } else { - const memberPendingProfileKey = new Proto.MemberPendingProfileKey(); - memberPendingProfileKey.member = member; - memberPendingProfileKey.addedByUserId = ourAciCipherTextBuffer; - memberPendingProfileKey.timestamp = Long.fromNumber(now); - - const addPendingMemberAction = - new Proto.GroupChange.Actions.AddMemberPendingProfileKeyAction(); - addPendingMemberAction.added = memberPendingProfileKey; - - addMembersPendingProfileKey.push(addPendingMemberAction); + addMembersPendingProfileKey.push({ + added: { + member: { ...member, presentation: null }, + addedByUserId: ourAciCipherTextBuffer, + timestamp: BigInt(now), + }, + }); } const doesMemberNeedUnban = conversation.bannedMembersV2?.some( @@ -638,13 +706,9 @@ export async function buildAddMembersChange( serviceId ); - const deleteMemberBannedAction = - new Proto.GroupChange.Actions.DeleteMemberBannedAction(); - - deleteMemberBannedAction.deletedUserId = uuidCipherTextBuffer; - - actions.deleteMembersBanned = actions.deleteMembersBanned || []; - actions.deleteMembersBanned.push(deleteMemberBannedAction); + deleteMembersBanned.push({ + deletedUserId: uuidCipherTextBuffer, + }); } }) ); @@ -654,15 +718,17 @@ export async function buildAddMembersChange( // will be logged. return undefined; } - if (addMembers.length) { - actions.addMembers = addMembers; - } - if (addMembersPendingProfileKey.length) { - actions.addMembersPendingProfileKey = addMembersPendingProfileKey; - } - actions.version = newGroupVersion; - return actions; + return toGroupActionsParams({ + version: newGroupVersion, + addMembers: addMembers.length ? addMembers : null, + addMembersPendingProfileKey: addMembersPendingProfileKey.length + ? addMembersPendingProfileKey + : null, + deleteMembersBanned: deleteMembersBanned.length + ? deleteMembersBanned + : null, + }); } export async function buildUpdateAttributesChange( @@ -675,7 +741,7 @@ export async function buildUpdateAttributesChange( description?: string; title?: string; }> -): Promise { +): Promise { const { publicParams, secretParams, revision, id } = conversation; const logId = `groupv2(${id})`; @@ -691,10 +757,6 @@ export async function buildUpdateAttributesChange( ); } - const actions = new Proto.GroupChange.Actions(); - - let hasChangedSomething = false; - const clientZkGroupCipher = getClientZkGroupCipher(secretParams); // There are three possible states here: @@ -702,10 +764,8 @@ export async function buildUpdateAttributesChange( // 1. 'avatar' not in attributes: we don't want to change the avatar. // 2. attributes.avatar === undefined: we want to clear the avatar. // 3. attributes.avatar !== undefined: we want to update the avatar. + let modifyAvatar: Actions.ModifyAvatarAction.Params | null = null; if ('avatar' in attributes) { - hasChangedSomething = true; - - actions.modifyAvatar = new Proto.GroupChange.Actions.ModifyAvatarAction(); const { avatar } = attributes; if (avatar) { const uploadedAvatar = await uploadAvatar({ @@ -714,44 +774,47 @@ export async function buildUpdateAttributesChange( publicParams, secretParams, }); - actions.modifyAvatar.avatar = uploadedAvatar.key; + modifyAvatar = { avatar: uploadedAvatar.key }; + } else { + modifyAvatar = { avatar: null }; } - - // If we don't set `actions.modifyAvatar.avatar`, it will be cleared. } const { title } = attributes; + let modifyTitle: Actions.ModifyTitleAction.Params | null = null; if (title) { - hasChangedSomething = true; - - actions.modifyTitle = new Proto.GroupChange.Actions.ModifyTitleAction(); - actions.modifyTitle.title = buildGroupTitleBuffer( - clientZkGroupCipher, - title - ); + modifyTitle = { + title: buildGroupTitleBuffer(clientZkGroupCipher, title), + }; } const { description } = attributes; + let modifyDescription: Actions.ModifyDescriptionAction.Params | null = null; if (typeof description === 'string') { - hasChangedSomething = true; - - actions.modifyDescription = - new Proto.GroupChange.Actions.ModifyDescriptionAction(); - actions.modifyDescription.description = buildGroupDescriptionBuffer( - clientZkGroupCipher, - description - ); + modifyDescription = { + description: buildGroupDescriptionBuffer( + clientZkGroupCipher, + description + ), + }; } - if (!hasChangedSomething) { + if ( + modifyAvatar == null && + modifyTitle == null && + modifyDescription == null + ) { // This shouldn't happen. When these actions are passed to `modifyGroupV2`, a warning // will be logged. return undefined; } - actions.version = (revision || 0) + 1; - - return actions; + return toGroupActionsParams({ + version: (revision || 0) + 1, + modifyAvatar, + modifyTitle, + modifyDescription, + }); } export function buildDisappearingMessagesTimerChange({ @@ -760,12 +823,7 @@ export function buildDisappearingMessagesTimerChange({ }: { expireTimer: DurationInSeconds; group: ConversationAttributesType; -}): Proto.GroupChange.Actions { - const actions = new Proto.GroupChange.Actions(); - - const blob = new Proto.GroupAttributeBlob(); - blob.disappearingMessagesDuration = expireTimer; - +}): Actions.Params { if (!group.secretParams) { throw new Error( 'buildDisappearingMessagesTimerChange: group was missing secretParams!' @@ -773,172 +831,147 @@ export function buildDisappearingMessagesTimerChange({ } const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); - const blobPlaintext = Proto.GroupAttributeBlob.encode(blob).finish(); + const blobPlaintext = Proto.GroupAttributeBlob.encode({ + content: { + disappearingMessagesDuration: expireTimer, + }, + }); const blobCipherText = encryptGroupBlob(clientZkGroupCipher, blobPlaintext); - const timerAction = - new Proto.GroupChange.Actions.ModifyDisappearingMessageTimerAction(); - timerAction.timer = blobCipherText; - - actions.version = (group.revision || 0) + 1; - actions.modifyDisappearingMessageTimer = timerAction; - - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyDisappearingMessageTimer: { + timer: blobCipherText, + }, + }); } export function buildInviteLinkPasswordChange( group: ConversationAttributesType, inviteLinkPassword: string -): Proto.GroupChange.Actions { - const inviteLinkPasswordAction = - new Proto.GroupChange.Actions.ModifyInviteLinkPasswordAction(); - inviteLinkPasswordAction.inviteLinkPassword = - Bytes.fromBase64(inviteLinkPassword); - - const actions = new Proto.GroupChange.Actions(); - actions.version = (group.revision || 0) + 1; - actions.modifyInviteLinkPassword = inviteLinkPasswordAction; - - return actions; +): Actions.Params { + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyInviteLinkPassword: { + inviteLinkPassword: Bytes.fromBase64(inviteLinkPassword), + }, + }); } export function buildNewGroupLinkChange( group: ConversationAttributesType, inviteLinkPassword: string, - addFromInviteLinkAccess: AccessRequiredEnum -): Proto.GroupChange.Actions { - const accessControlAction = - new Proto.GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction(); - accessControlAction.addFromInviteLinkAccess = addFromInviteLinkAccess; - - const inviteLinkPasswordAction = - new Proto.GroupChange.Actions.ModifyInviteLinkPasswordAction(); - inviteLinkPasswordAction.inviteLinkPassword = - Bytes.fromBase64(inviteLinkPassword); - - const actions = new Proto.GroupChange.Actions(); - actions.version = (group.revision || 0) + 1; - actions.modifyAddFromInviteLinkAccess = accessControlAction; - actions.modifyInviteLinkPassword = inviteLinkPasswordAction; - - return actions; + addFromInviteLinkAccess: AccessRequired +): Actions.Params { + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyAddFromInviteLinkAccess: { + addFromInviteLinkAccess, + }, + modifyInviteLinkPassword: { + inviteLinkPassword: Bytes.fromBase64(inviteLinkPassword), + }, + }); } export function buildAccessControlAddFromInviteLinkChange( group: ConversationAttributesType, - value: AccessRequiredEnum -): Proto.GroupChange.Actions { - const accessControlAction = - new Proto.GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction(); - accessControlAction.addFromInviteLinkAccess = value; - - const actions = new Proto.GroupChange.Actions(); - actions.version = (group.revision || 0) + 1; - actions.modifyAddFromInviteLinkAccess = accessControlAction; - - return actions; + addFromInviteLinkAccess: AccessRequired +): Actions.Params { + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyAddFromInviteLinkAccess: { + addFromInviteLinkAccess, + }, + }); } export function buildAnnouncementsOnlyChange( group: ConversationAttributesType, - value: boolean -): Proto.GroupChange.Actions { - const action = new Proto.GroupChange.Actions.ModifyAnnouncementsOnlyAction(); - action.announcementsOnly = value; - - const actions = new Proto.GroupChange.Actions(); - actions.version = (group.revision || 0) + 1; - actions.modifyAnnouncementsOnly = action; - - return actions; + announcementsOnly: boolean +): Actions.Params { + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyAnnouncementsOnly: { + announcementsOnly, + }, + }); } export function buildAccessControlAttributesChange( group: ConversationAttributesType, - newValue: AccessRequiredEnum -): Proto.GroupChange.Actions { - const accessControlAction = - new Proto.GroupChange.Actions.ModifyAttributesAccessControlAction(); - accessControlAction.attributesAccess = newValue; - - const actions = new Proto.GroupChange.Actions(); - actions.version = (group.revision || 0) + 1; - actions.modifyAttributesAccess = accessControlAction; - - return actions; + attributesAccess: AccessRequired +): Actions.Params { + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyAttributesAccess: { + attributesAccess, + }, + }); } export function buildAccessControlMembersChange( group: ConversationAttributesType, - value: AccessRequiredEnum -): Proto.GroupChange.Actions { - const accessControlAction = - new Proto.GroupChange.Actions.ModifyMembersAccessControlAction(); - accessControlAction.membersAccess = value; - - const actions = new Proto.GroupChange.Actions(); - actions.version = (group.revision || 0) + 1; - actions.modifyMemberAccess = accessControlAction; - - return actions; + membersAccess: AccessRequired +): Actions.Params { + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyMemberAccess: { + membersAccess, + }, + }); } export function buildAccessControlMemberLabelChange( group: ConversationAttributesType, - value: AccessRequiredEnum -): Proto.GroupChange.Actions { - const ACCESS_ENUM = Proto.AccessControl.AccessRequired; - const ROLE_ENUM = Proto.Member.Role; - + memberLabelAccess: AccessRequired +): Actions.Params { if (!group.secretParams) { throw new Error( 'buildAccessControlMemberLabelChange: group was missing secretParams!' ); } - const accessControlAction = - new Proto.GroupChange.Actions.ModifyMemberLabelAccessControlAction(); - accessControlAction.memberLabelAccess = value; - - const actions = new Proto.GroupChange.Actions(); - actions.version = (group.revision || 0) + 1; - actions.modifyMemberLabelAccess = accessControlAction; + const modifyMemberLabels: Array = []; // Clear out all non-admin labels const previousValue = group.accessControl?.memberLabel; if ( - previousValue !== ACCESS_ENUM.ADMINISTRATOR && - value === ACCESS_ENUM.ADMINISTRATOR + previousValue !== AccessRequired.ADMINISTRATOR && + memberLabelAccess === AccessRequired.ADMINISTRATOR ) { const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); - const modifyLabelActions = (group.membersV2 || []) - .map(member => { - if (member.role === ROLE_ENUM.ADMINISTRATOR) { - return undefined; - } + for (const member of group.membersV2 ?? []) { + if (member.role === MemberRole.ADMINISTRATOR) { + continue; + } - if (!member.labelString && !member.labelEmoji) { - return undefined; - } + if (!member.labelString && !member.labelEmoji) { + continue; + } - const modifyLabel = - new Proto.GroupChange.Actions.ModifyMemberLabelAction(); - modifyLabel.userId = encryptServiceId(clientZkGroupCipher, member.aci); + modifyMemberLabels.push({ + userId: encryptServiceId(clientZkGroupCipher, member.aci), + labelEmoji: null, + labelString: null, + }); + } - return modifyLabel; - }) - .filter(isNotNil); - - if (modifyLabelActions.length) { + if (modifyMemberLabels.length) { log.info( - `buildAccessControlMemberLabelChange: Found ${modifyLabelActions.length} non-admins with labels. Clearing.` + `buildAccessControlMemberLabelChange: Found ${modifyMemberLabels.length} non-admins with labels. Clearing.` ); - actions.modifyMemberLabels = modifyLabelActions; } } - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyMemberLabelAccess: { + memberLabelAccess, + }, + modifyMemberLabels: modifyMemberLabels.length ? modifyMemberLabels : null, + }); } export function _maybeBuildAddBannedMemberActions({ @@ -952,14 +985,17 @@ export function _maybeBuildAddBannedMemberActions({ ourAci: AciString; serviceId: ServiceIdString; }): Pick< - Proto.GroupChange.IActions, + Proto.GroupChange.Actions.Params, 'addMembersBanned' | 'deleteMembersBanned' > { const doesMemberNeedBan = !group.bannedMembersV2?.some(member => member.serviceId === serviceId) && serviceId !== ourAci; if (!doesMemberNeedBan) { - return {}; + return { + addMembersBanned: null, + deleteMembersBanned: null, + }; } // Sort current banned members by decreasing timestamp const sortedBannedMembers = [...(group.bannedMembersV2 ?? [])].sort( @@ -974,30 +1010,28 @@ export function _maybeBuildAddBannedMemberActions({ Math.max(0, getGroupSizeHardLimit() - 1) ); - let deleteMembersBanned = null; + let deleteMembersBanned: Array | null = + null; if (deletedBannedMembers.length > 0) { deleteMembersBanned = deletedBannedMembers.map(bannedMember => { - const deleteMemberBannedAction = - new Proto.GroupChange.Actions.DeleteMemberBannedAction(); - - deleteMemberBannedAction.deletedUserId = encryptServiceId( - clientZkGroupCipher, - bannedMember.serviceId - ); - - return deleteMemberBannedAction; + return { + deletedUserId: encryptServiceId( + clientZkGroupCipher, + bannedMember.serviceId + ), + }; }); } - const addMemberBannedAction = - new Proto.GroupChange.Actions.AddMemberBannedAction(); - - const uuidCipherTextBuffer = encryptServiceId(clientZkGroupCipher, serviceId); - addMemberBannedAction.added = new Proto.MemberBanned(); - addMemberBannedAction.added.userId = uuidCipherTextBuffer; - return { - addMembersBanned: [addMemberBannedAction], + addMembersBanned: [ + { + added: { + userId: encryptServiceId(clientZkGroupCipher, serviceId), + timestamp: null, + }, + }, + ], deleteMembersBanned, }; } @@ -1011,9 +1045,7 @@ export function buildDeletePendingAdminApprovalMemberChange({ group: ConversationAttributesType; ourAci: AciString; aci: AciString; -}): Proto.GroupChange.Actions { - const actions = new Proto.GroupChange.Actions(); - +}): Actions.Params { if (!group.secretParams) { throw new Error( 'buildDeletePendingAdminApprovalMemberChange: group was missing secretParams!' @@ -1022,15 +1054,6 @@ export function buildDeletePendingAdminApprovalMemberChange({ const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const uuidCipherTextBuffer = encryptServiceId(clientZkGroupCipher, aci); - const deleteMemberPendingAdminApproval = - new Proto.GroupChange.Actions.DeleteMemberPendingAdminApprovalAction(); - deleteMemberPendingAdminApproval.deletedUserId = uuidCipherTextBuffer; - - actions.version = (group.revision || 0) + 1; - actions.deleteMembersPendingAdminApproval = [ - deleteMemberPendingAdminApproval, - ]; - const { addMembersBanned, deleteMembersBanned } = _maybeBuildAddBannedMemberActions({ clientZkGroupCipher, @@ -1039,14 +1062,16 @@ export function buildDeletePendingAdminApprovalMemberChange({ serviceId: aci, }); - if (addMembersBanned) { - actions.addMembersBanned = addMembersBanned; - } - if (deleteMembersBanned) { - actions.deleteMembersBanned = deleteMembersBanned; - } - - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + deleteMembersPendingAdminApproval: [ + { + deletedUserId: uuidCipherTextBuffer, + }, + ], + addMembersBanned, + deleteMembersBanned, + }); } export function buildAddPendingAdminApprovalMemberChange({ @@ -1057,9 +1082,7 @@ export function buildAddPendingAdminApprovalMemberChange({ group: ConversationAttributesType; profileKeyCredentialBase64: string; serverPublicParamsBase64: string; -}): Proto.GroupChange.Actions { - const actions = new Proto.GroupChange.Actions(); - +}): Actions.Params { if (!group.secretParams) { throw new Error( 'buildAddPendingAdminApprovalMemberChange: group was missing secretParams!' @@ -1069,23 +1092,25 @@ export function buildAddPendingAdminApprovalMemberChange({ serverPublicParamsBase64 ); - const addMemberPendingAdminApproval = - new Proto.GroupChange.Actions.AddMemberPendingAdminApprovalAction(); const presentation = createProfileKeyCredentialPresentation( clientZkProfileCipher, profileKeyCredentialBase64, group.secretParams ); - const added = new Proto.MemberPendingAdminApproval(); - added.presentation = presentation; - - addMemberPendingAdminApproval.added = added; - - actions.version = (group.revision || 0) + 1; - actions.addMembersPendingAdminApproval = [addMemberPendingAdminApproval]; - - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + addMembersPendingAdminApproval: [ + { + added: { + presentation, + userId: null, + profileKey: null, + timestamp: null, + }, + }, + ], + }); } export function buildAddMember({ @@ -1099,11 +1124,7 @@ export function buildAddMember({ serverPublicParamsBase64: string; joinFromInviteLink?: boolean; serviceId: ServiceIdString; -}): Proto.GroupChange.Actions { - const MEMBER_ROLE_ENUM = Proto.Member.Role; - - const actions = new Proto.GroupChange.Actions(); - +}): Actions.Params { if (!group.secretParams) { throw new Error('buildAddMember: group was missing secretParams!'); } @@ -1111,37 +1132,41 @@ export function buildAddMember({ serverPublicParamsBase64 ); - const addMember = new Proto.GroupChange.Actions.AddMemberAction(); const presentation = createProfileKeyCredentialPresentation( clientZkProfileCipher, profileKeyCredentialBase64, group.secretParams ); - const added = new Proto.Member(); - added.presentation = presentation; - added.role = MEMBER_ROLE_ENUM.DEFAULT; + const doesMemberNeedUnban = group.bannedMembersV2?.some(member => { + return member.serviceId === serviceId; + }); - addMember.added = added; - - actions.version = (group.revision || 0) + 1; - actions.addMembers = [addMember]; - - const doesMemberNeedUnban = group.bannedMembersV2?.some( - member => member.serviceId === serviceId - ); + let deletedUserId: Uint8Array | null = null; if (doesMemberNeedUnban) { const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const userIdCipherText = encryptServiceId(clientZkGroupCipher, serviceId); - - const deleteMemberBannedAction = - new Proto.GroupChange.Actions.DeleteMemberBannedAction(); - - deleteMemberBannedAction.deletedUserId = userIdCipherText; - actions.deleteMembersBanned = [deleteMemberBannedAction]; + deletedUserId = userIdCipherText; } - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + addMembers: [ + { + added: { + presentation, + role: MemberRole.DEFAULT, + userId: null, + profileKey: null, + joinedAtVersion: null, + labelEmoji: null, + labelString: null, + }, + joinFromInviteLink: null, + }, + ], + deleteMembersBanned: deletedUserId != null ? [{ deletedUserId }] : null, + }); } export function buildDeletePendingMemberChange({ @@ -1150,9 +1175,7 @@ export function buildDeletePendingMemberChange({ }: { serviceIds: ReadonlyArray; group: ConversationAttributesType; -}): Proto.GroupChange.Actions { - const actions = new Proto.GroupChange.Actions(); - +}): Actions.Params { if (!group.secretParams) { throw new Error( 'buildDeletePendingMemberChange: group was missing secretParams!' @@ -1160,21 +1183,14 @@ export function buildDeletePendingMemberChange({ } const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); - const deleteMembersPendingProfileKey = serviceIds.map(serviceId => { - const uuidCipherTextBuffer = encryptServiceId( - clientZkGroupCipher, - serviceId - ); - const deletePendingMember = - new Proto.GroupChange.Actions.DeleteMemberPendingProfileKeyAction(); - deletePendingMember.deletedUserId = uuidCipherTextBuffer; - return deletePendingMember; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + deleteMembersPendingProfileKey: serviceIds.map(serviceId => { + return { + deletedUserId: encryptServiceId(clientZkGroupCipher, serviceId), + }; + }), }); - - actions.version = (group.revision || 0) + 1; - actions.deleteMembersPendingProfileKey = deleteMembersPendingProfileKey; - - return actions; } export function buildDeleteMemberChange({ @@ -1185,21 +1201,13 @@ export function buildDeleteMemberChange({ group: ConversationAttributesType; ourAci: AciString; serviceId: ServiceIdString; -}): Proto.GroupChange.Actions { - const actions = new Proto.GroupChange.Actions(); - +}): Actions.Params { if (!group.secretParams) { throw new Error('buildDeleteMemberChange: group was missing secretParams!'); } const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const uuidCipherTextBuffer = encryptServiceId(clientZkGroupCipher, serviceId); - const deleteMember = new Proto.GroupChange.Actions.DeleteMemberAction(); - deleteMember.deletedUserId = uuidCipherTextBuffer; - - actions.version = (group.revision || 0) + 1; - actions.deleteMembers = [deleteMember]; - const { addMembersBanned, deleteMembersBanned } = _maybeBuildAddBannedMemberActions({ clientZkGroupCipher, @@ -1208,14 +1216,12 @@ export function buildDeleteMemberChange({ serviceId, }); - if (addMembersBanned) { - actions.addMembersBanned = addMembersBanned; - } - if (deleteMembersBanned) { - actions.deleteMembersBanned = deleteMembersBanned; - } - - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + deleteMembers: [{ deletedUserId: uuidCipherTextBuffer }], + addMembersBanned, + deleteMembersBanned, + }); } export function buildAddBannedMemberChange({ @@ -1224,9 +1230,7 @@ export function buildAddBannedMemberChange({ }: { serviceId: ServiceIdString; group: ConversationAttributesType; -}): Proto.GroupChange.Actions { - const actions = new Proto.GroupChange.Actions(); - +}): Actions.Params { if (!group.secretParams) { throw new Error( 'buildAddBannedMemberChange: group was missing secretParams!' @@ -1235,28 +1239,27 @@ export function buildAddBannedMemberChange({ const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const userIdCipherText = encryptServiceId(clientZkGroupCipher, serviceId); - const addMemberBannedAction = - new Proto.GroupChange.Actions.AddMemberBannedAction(); + const needsDeleteFromPendingAdminApproval = + group.pendingAdminApprovalV2?.some(item => item.aci === serviceId); - addMemberBannedAction.added = new Proto.MemberBanned(); - addMemberBannedAction.added.userId = userIdCipherText; - - actions.addMembersBanned = [addMemberBannedAction]; - - if (group.pendingAdminApprovalV2?.some(item => item.aci === serviceId)) { - const deleteMemberPendingAdminApprovalAction = - new Proto.GroupChange.Actions.DeleteMemberPendingAdminApprovalAction(); - - deleteMemberPendingAdminApprovalAction.deletedUserId = userIdCipherText; - - actions.deleteMembersPendingAdminApproval = [ - deleteMemberPendingAdminApprovalAction, - ]; - } - - actions.version = (group.revision || 0) + 1; - - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + addMembersBanned: [ + { + added: { + userId: userIdCipherText, + timestamp: null, + }, + }, + ], + deleteMembersPendingAdminApproval: needsDeleteFromPendingAdminApproval + ? [ + { + deletedUserId: userIdCipherText, + }, + ] + : null, + }); } export function buildModifyMemberRoleChange({ @@ -1267,9 +1270,7 @@ export function buildModifyMemberRoleChange({ serviceId: ServiceIdString; group: ConversationAttributesType; role: number; -}): Proto.GroupChange.Actions { - const actions = new Proto.GroupChange.Actions(); - +}): Actions.Params { if (!group.secretParams) { throw new Error( 'buildModifyMemberRoleChange: group was missing secretParams!' @@ -1279,33 +1280,37 @@ export function buildModifyMemberRoleChange({ const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const userIdCipherText = encryptServiceId(clientZkGroupCipher, serviceId); - const toggleAdmin = new Proto.GroupChange.Actions.ModifyMemberRoleAction(); - toggleAdmin.userId = userIdCipherText; - toggleAdmin.role = role; - - actions.version = (group.revision || 0) + 1; - actions.modifyMemberRoles = [toggleAdmin]; - const membership = group.membersV2?.find(member => member.aci === serviceId); const onlyAdminsCanAddMemberLabel = group.accessControl?.memberLabel === Proto.AccessControl.AccessRequired.ADMINISTRATOR; - const wasPreviouslyAnAdmin = - membership?.role === Proto.Member.Role.ADMINISTRATOR; - const nowNotAnAdmin = role !== Proto.Member.Role.ADMINISTRATOR; + const wasPreviouslyAnAdmin = membership?.role === MemberRole.ADMINISTRATOR; + const nowNotAnAdmin = role !== MemberRole.ADMINISTRATOR; - if ( + const shouldDropMemberLabel = membership?.labelString && onlyAdminsCanAddMemberLabel && wasPreviouslyAnAdmin && - nowNotAnAdmin - ) { - const modifyLabel = new Proto.GroupChange.Actions.ModifyMemberLabelAction(); - modifyLabel.userId = userIdCipherText; - actions.modifyMemberLabels = [modifyLabel]; - } + nowNotAnAdmin; - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyMemberRoles: [ + { + userId: userIdCipherText, + role, + }, + ], + modifyMemberLabels: shouldDropMemberLabel + ? [ + { + userId: userIdCipherText, + labelEmoji: null, + labelString: null, + }, + ] + : null, + }); } export function buildModifyMemberLabelChange({ @@ -1318,8 +1323,7 @@ export function buildModifyMemberLabelChange({ group: ConversationAttributesType; labelEmoji: string | undefined; labelString: string | undefined; -}): Proto.GroupChange.Actions { - const actions = new Proto.GroupChange.Actions(); +}): Actions.Params { const logId = `buildModifyMemberLabelChange(${getConversationIdForLogging(group)})`; if (!group.secretParams) { @@ -1329,8 +1333,9 @@ export function buildModifyMemberLabelChange({ const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); const userIdCipherText = encryptServiceId(clientZkGroupCipher, serviceId); - const modifyLabel = new Proto.GroupChange.Actions.ModifyMemberLabelAction(); - modifyLabel.userId = userIdCipherText; + let encryptedLabelEmoji: Uint8Array | null = null; + let encryptedLabelString: Uint8Array | null = null; + if (labelEmoji) { const labelEmojiBytes = Bytes.fromString(labelEmoji); @@ -1340,36 +1345,42 @@ export function buildModifyMemberLabelChange({ ); } - modifyLabel.labelEmoji = encryptGroupBlob( + encryptedLabelEmoji = encryptGroupBlob( clientZkGroupCipher, labelEmojiBytes ); - if (modifyLabel.labelEmoji.byteLength > SERVER_EMOJI_BYTE_LIMIT) { + if (encryptedLabelEmoji.byteLength > SERVER_EMOJI_BYTE_LIMIT) { throw new Error( - `${logId}: encrypted label emoji length (${modifyLabel.labelEmoji.length}) is larger than limit (${SERVER_EMOJI_BYTE_LIMIT})!` + `${logId}: encrypted label emoji length (${encryptedLabelEmoji.byteLength}) is larger than limit (${SERVER_EMOJI_BYTE_LIMIT})!` ); } } if (labelString) { - modifyLabel.labelString = encryptGroupBlob( + encryptedLabelString = encryptGroupBlob( clientZkGroupCipher, Bytes.fromString(labelString) ); - if (modifyLabel.labelString.byteLength > SERVER_STRING_BYTE_LIMIT) { + if (encryptedLabelString.byteLength > SERVER_STRING_BYTE_LIMIT) { throw new Error( - `${logId} encrypted label string length (${modifyLabel.labelString.length}) is larger than limit (${SERVER_STRING_BYTE_LIMIT})!` + `${logId} encrypted label string length (${encryptedLabelString.byteLength}) is larger than limit (${SERVER_STRING_BYTE_LIMIT})!` ); } } - if (modifyLabel.labelEmoji && !modifyLabel.labelString) { + if (encryptedLabelEmoji && !encryptedLabelString) { throw new Error(`${logId} labelEmoji was provided, but not labelString!`); } - actions.version = (group.revision || 0) + 1; - actions.modifyMemberLabels = [modifyLabel]; - - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + modifyMemberLabels: [ + { + userId: userIdCipherText, + labelEmoji: encryptedLabelEmoji, + labelString: encryptedLabelString, + }, + ], + }); } export function buildPromotePendingAdminApprovalMemberChange({ @@ -1378,10 +1389,7 @@ export function buildPromotePendingAdminApprovalMemberChange({ }: { group: ConversationAttributesType; aci: AciString; -}): Proto.GroupChange.Actions { - const MEMBER_ROLE_ENUM = Proto.Member.Role; - const actions = new Proto.GroupChange.Actions(); - +}): Actions.Params { if (!group.secretParams) { throw new Error( 'buildAddPendingAdminApprovalMemberChange: group was missing secretParams!' @@ -1389,17 +1397,17 @@ export function buildPromotePendingAdminApprovalMemberChange({ } const clientZkGroupCipher = getClientZkGroupCipher(group.secretParams); - const userIdCipher = encryptServiceId(clientZkGroupCipher, aci); + const userIdCipherText = encryptServiceId(clientZkGroupCipher, aci); - const promotePendingMember = - new Proto.GroupChange.Actions.PromoteMemberPendingAdminApprovalAction(); - promotePendingMember.userId = userIdCipher; - promotePendingMember.role = MEMBER_ROLE_ENUM.DEFAULT; - - actions.version = (group.revision || 0) + 1; - actions.promoteMembersPendingAdminApproval = [promotePendingMember]; - - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + promoteMembersPendingAdminApproval: [ + { + userId: userIdCipherText, + role: MemberRole.DEFAULT, + }, + ], + }); } export type BuildPromoteMemberChangeOptionsType = Readonly<{ @@ -1414,17 +1422,13 @@ export function buildPromoteMemberChange({ profileKeyCredentialBase64, serverPublicParamsBase64, isPendingPniAciProfileKey = false, -}: BuildPromoteMemberChangeOptionsType): Proto.GroupChange.Actions { - const actions = new Proto.GroupChange.Actions(); - +}: BuildPromoteMemberChangeOptionsType): Actions.Params { if (!group.secretParams) { throw new Error( 'buildDisappearingMessagesTimerChange: group was missing secretParams!' ); } - actions.version = (group.revision || 0) + 1; - const clientZkProfileCipher = getClientZkProfileOperations( serverPublicParamsBase64 ); @@ -1435,21 +1439,29 @@ export function buildPromoteMemberChange({ group.secretParams ); - if (isPendingPniAciProfileKey) { - actions.promoteMembersPendingPniAciProfileKey = [ - { - presentation, - }, - ]; - } else { - actions.promoteMembersPendingProfileKey = [ - { - presentation, - }, - ]; - } - - return actions; + return toGroupActionsParams({ + version: (group.revision || 0) + 1, + ...(isPendingPniAciProfileKey + ? { + promoteMembersPendingPniAciProfileKey: [ + { + presentation, + userId: null, + pni: null, + profileKey: null, + }, + ], + } + : { + promoteMembersPendingProfileKey: [ + { + presentation, + userId: null, + profileKey: null, + }, + ], + }), + }); } async function uploadGroupChange({ @@ -1459,12 +1471,12 @@ async function uploadGroupChange({ groupSecretParamsBase64, inviteLinkPassword, }: { - actions: Proto.GroupChange.IActions; + actions: Proto.GroupChange.Actions.Params; groupId: string; groupPublicParamsBase64: string; groupSecretParamsBase64: string; inviteLinkPassword?: string; -}): Promise { +}): Promise { const logId = idForLogging(groupId); // Ensure we have the credentials we need before attempting GroupsV2 operations @@ -1489,7 +1501,7 @@ export async function modifyGroupV2({ }: { conversation: ConversationModel; usingCredentialsFrom: ReadonlyArray; - createGroupChange: () => Promise; + createGroupChange: () => Promise; extraConversationsForSend?: ReadonlyArray; inviteLinkPassword?: string; name: string; @@ -1578,8 +1590,7 @@ export async function modifyGroupV2({ groupChangeResponse; strictAssert(groupChange, 'modifyGroupV2: missing groupChange'); - const groupChangeBuffer = - Proto.GroupChange.encode(groupChange).finish(); + const groupChangeBuffer = Proto.GroupChange.encode(groupChange); const groupChangeBase64 = Bytes.toBase64(groupChangeBuffer); // Apply change locally, just like we would with an incoming change. This will @@ -1800,9 +1811,6 @@ export async function createGroupV2( // Ensure we have the credentials we need before attempting GroupsV2 operations await maybeFetchNewCredentials(); - const ACCESS_ENUM = Proto.AccessControl.AccessRequired; - const MEMBER_ROLE_ENUM = Proto.Member.Role; - const masterKeyBuffer = getRandomBytes(32); const fields = deriveGroupFields(masterKeyBuffer); @@ -1825,7 +1833,7 @@ export async function createGroupV2( const membersV2: Array = [ { aci: ourAci, - role: MEMBER_ROLE_ENUM.ADMINISTRATOR, + role: MemberRole.ADMINISTRATOR, joinedAtVersion: 0, }, ]; @@ -1862,7 +1870,7 @@ export async function createGroupV2( strictAssert(isAciString(contactServiceId), 'profile key without ACI'); membersV2.push({ aci: contactServiceId, - role: MEMBER_ROLE_ENUM.DEFAULT, + role: MemberRole.DEFAULT, joinedAtVersion: 0, }); } else { @@ -1870,7 +1878,7 @@ export async function createGroupV2( addedByUserId: ourAci, serviceId: contactServiceId, timestamp: Date.now(), - role: MEMBER_ROLE_ENUM.DEFAULT, + role: MemberRole.DEFAULT, }); } }), @@ -1904,10 +1912,10 @@ export async function createGroupV2( // GroupV2 state accessControl: { - attributes: ACCESS_ENUM.MEMBER, - members: ACCESS_ENUM.MEMBER, - addFromInviteLink: ACCESS_ENUM.UNSATISFIABLE, - memberLabel: ACCESS_ENUM.MEMBER, + attributes: AccessRequired.MEMBER, + members: AccessRequired.MEMBER, + addFromInviteLink: AccessRequired.UNSATISFIABLE, + memberLabel: AccessRequired.MEMBER, }, membersV2, pendingMembersV2, @@ -2172,7 +2180,6 @@ export async function getGroupMigrationMembers( previousGroupV1Members: Array; }> { const logId = conversation.idForLogging(); - const MEMBER_ROLE_ENUM = Proto.Member.Role; const ourConversationId = window.ConversationController.getOurConversationId(); @@ -2247,7 +2254,7 @@ export async function getGroupMigrationMembers( return { aci: contactAci, - role: MEMBER_ROLE_ENUM.ADMINISTRATOR, + role: MemberRole.ADMINISTRATOR, joinedAtVersion: 0, }; }) @@ -2299,7 +2306,7 @@ export async function getGroupMigrationMembers( serviceId: contactUuid, timestamp: now, addedByUserId: ourAci, - role: MEMBER_ROLE_ENUM.ADMINISTRATOR, + role: MemberRole.ADMINISTRATOR, }; }) ); @@ -2329,8 +2336,6 @@ export async function initiateMigrationToGroupV2( try { await conversation.queueJob('initiateMigrationToGroupV2', async () => { - const ACCESS_ENUM = Proto.AccessControl.AccessRequired; - const isEligible = await isGroupEligibleToMigrate(conversation); const previousGroupV1Id = conversation.get('groupId'); @@ -2420,10 +2425,10 @@ export async function initiateMigrationToGroupV2( // GroupV2 state accessControl: { - attributes: ACCESS_ENUM.MEMBER, - members: ACCESS_ENUM.MEMBER, - addFromInviteLink: ACCESS_ENUM.UNSATISFIABLE, - memberLabel: ACCESS_ENUM.MEMBER, + attributes: AccessRequired.MEMBER, + members: AccessRequired.MEMBER, + addFromInviteLink: AccessRequired.UNSATISFIABLE, + memberLabel: AccessRequired.MEMBER, }, membersV2, pendingMembersV2, @@ -2784,7 +2789,7 @@ export async function respondToGroupV2Migration({ members: undefined, }; - let firstGroupState: Proto.IGroup | null | undefined; + let firstGroupState: Proto.Group.Params | null | undefined; let groupSendEndorsementsResponse: Uint8Array | null | undefined; try { @@ -3911,7 +3916,7 @@ async function updateGroupViaSingleChange({ newRevision, }: { group: ConversationAttributesType; - groupChange: Proto.IGroupChange; + groupChange: Proto.GroupChange.Params; newRevision: number; }): Promise { const previouslyKnewAboutThisGroup = @@ -3984,7 +3989,7 @@ async function updateGroupViaSingleChange({ } function getLastRevisionFromChanges( - changes: ReadonlyArray + changes: ReadonlyArray ): number | undefined { for (let i = changes.length - 1; i >= 0; i -= 1) { const change = changes[i]; @@ -4070,7 +4075,7 @@ async function updateGroupViaLogs({ let response: GroupLogResponseType; let groupSendEndorsementsResponse: Uint8Array | null = null; - const changes: Array = []; + const changes: Array = []; do { const fetchedAt = Date.now(); // eslint-disable-next-line no-await-in-loop @@ -4255,7 +4260,7 @@ async function integrateGroupChanges({ }: { group: ConversationAttributesType; newRevision: number | undefined; - changes: ReadonlyArray; + changes: ReadonlyArray; }): Promise { const logId = idForLogging(group.groupId); let attributes = group; @@ -4354,8 +4359,8 @@ async function integrateGroupChange({ newRevision, }: { group: ConversationAttributesType; - groupChange?: Proto.IGroupChange; - groupState?: Proto.IGroup; + groupChange?: Proto.GroupChange.Params; + groupState?: Proto.Group.Params; newRevision: number | undefined; }): Promise { const logId = idForLogging(group.groupId); @@ -4385,7 +4390,7 @@ async function integrateGroupChange({ let isChangeSupported = false; let isSameVersion = false; let isMoreThanOneVersionUp = false; - let groupChangeActions: undefined | Proto.GroupChange.IActions; + let groupChangeActions: undefined | Proto.GroupChange.Actions; let decryptedChangeActions: undefined | DecryptedGroupChangeActions; let sourceServiceId: undefined | ServiceIdString; @@ -5171,9 +5176,6 @@ async function applyGroupChange({ const logId = idForLogging(group.groupId); const ourAci = itemStorage.user.getCheckedAci(); - const ACCESS_ENUM = Proto.AccessControl.AccessRequired; - const MEMBER_ROLE_ENUM = Proto.Member.Role; - const version = actions.version || 0; let result = { ...group }; const newProfileKeys: Array = []; @@ -5223,7 +5225,7 @@ async function applyGroupChange({ members[addedUuid] = { aci: addedUuid, - role: added.role || MEMBER_ROLE_ENUM.DEFAULT, + role: added.role || MemberRole.DEFAULT, joinedAtVersion: version, joinedFromLink: addMember.joinFromInviteLink || false, }; @@ -5370,7 +5372,7 @@ async function applyGroupChange({ serviceId: addedUserId, addedByUserId: added.addedByUserId, timestamp: added.timestamp, - role: added.member.role || MEMBER_ROLE_ENUM.DEFAULT, + role: added.member.role || MemberRole.DEFAULT, }; } ); @@ -5430,7 +5432,7 @@ async function applyGroupChange({ members[aci] = { aci, joinedAtVersion: version, - role: previousRecord.role || MEMBER_ROLE_ENUM.DEFAULT, + role: previousRecord.role || MemberRole.DEFAULT, }; newProfileKeys.push({ @@ -5474,7 +5476,7 @@ async function applyGroupChange({ members[aci] = { aci, joinedAtVersion: version, - role: previousRecord.role || MEMBER_ROLE_ENUM.DEFAULT, + role: previousRecord.role || MemberRole.DEFAULT, }; newProfileKeys.push({ @@ -5486,9 +5488,9 @@ async function applyGroupChange({ // modifyTitle?: GroupChange.Actions.ModifyTitleAction; if (actions.modifyTitle) { - const { title } = actions.modifyTitle; - if (title && title.content === 'title') { - result.name = dropNull(title.title)?.trim(); + const title = actions.modifyTitle.title?.content?.title; + if (title != null) { + result.name = title.trim(); } else { log.warn( `applyGroupChange/${logId}: Clearing group title due to missing data.` @@ -5513,15 +5515,13 @@ async function applyGroupChange({ // modifyDisappearingMessageTimer?: // GroupChange.Actions.ModifyDisappearingMessageTimerAction; if (actions.modifyDisappearingMessageTimer) { - const disappearingMessagesTimer: Proto.GroupAttributeBlob | undefined = - actions.modifyDisappearingMessageTimer.timer; - if ( - disappearingMessagesTimer && - disappearingMessagesTimer.content === 'disappearingMessagesDuration' - ) { - const duration = disappearingMessagesTimer.disappearingMessagesDuration; - result.expireTimer = - duration == null ? undefined : DurationInSeconds.fromSeconds(duration); + const disappearingMessagesDuration = + actions.modifyDisappearingMessageTimer.timer?.content + ?.disappearingMessagesDuration; + if (disappearingMessagesDuration != null) { + result.expireTimer = DurationInSeconds.fromSeconds( + disappearingMessagesDuration + ); } else { log.warn( `applyGroupChange/${logId}: Clearing group expireTimer due to missing data.` @@ -5530,50 +5530,50 @@ async function applyGroupChange({ } } - result.accessControl = result.accessControl || { - members: ACCESS_ENUM.MEMBER, - attributes: ACCESS_ENUM.MEMBER, - addFromInviteLink: ACCESS_ENUM.UNSATISFIABLE, - memberLabel: ACCESS_ENUM.MEMBER, + const accessControl = { + members: AccessRequired.MEMBER, + attributes: AccessRequired.MEMBER, + addFromInviteLink: AccessRequired.UNSATISFIABLE, + memberLabel: AccessRequired.MEMBER, + // Create a copy with defaults + ...result.accessControl, }; // modifyAttributesAccess?: // GroupChange.Actions.ModifyAttributesAccessControlAction; + if (actions.modifyAttributesAccess) { - result.accessControl = { - ...result.accessControl, - attributes: - actions.modifyAttributesAccess.attributesAccess || ACCESS_ENUM.MEMBER, - }; + const access = actions.modifyAttributesAccess?.attributesAccess; + accessControl.attributes = isValidAccess(access) + ? access + : AccessRequired.MEMBER; } // modifyMemberAccess?: GroupChange.Actions.ModifyMembersAccessControlAction; if (actions.modifyMemberAccess) { - result.accessControl = { - ...result.accessControl, - members: actions.modifyMemberAccess.membersAccess || ACCESS_ENUM.MEMBER, - }; + const access = actions.modifyMemberAccess?.membersAccess; + accessControl.members = isValidAccess(access) + ? access + : AccessRequired.MEMBER; } // modifyAddFromInviteLinkAccess?: // GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction; if (actions.modifyAddFromInviteLinkAccess) { - result.accessControl = { - ...result.accessControl, - addFromInviteLink: - actions.modifyAddFromInviteLinkAccess.addFromInviteLinkAccess || - ACCESS_ENUM.UNSATISFIABLE, - }; + const linkAccess = + actions.modifyAddFromInviteLinkAccess?.addFromInviteLinkAccess; + accessControl.members = isValidLinkAccess(linkAccess) + ? linkAccess + : AccessRequired.UNSATISFIABLE; } // modify_member_label_access?: // GroupChange.Actions.ModifyMemberLabelAccessControlAction; if (actions.modifyMemberLabelAccess) { - result.accessControl = { - ...result.accessControl, - memberLabel: - actions.modifyMemberLabelAccess.memberLabelAccess || ACCESS_ENUM.MEMBER, - }; + const access = actions.modifyMemberLabelAccess?.memberLabelAccess; + accessControl.members = isValidAccess(access) + ? access + : AccessRequired.MEMBER; } // addMembersPendingAdminApproval?: Array< @@ -5685,7 +5685,7 @@ async function applyGroupChange({ members[userId] = { aci: userId, joinedAtVersion: version, - role: role || MEMBER_ROLE_ENUM.DEFAULT, + role: role || MemberRole.DEFAULT, approvedByAdmin: true, }; } @@ -5703,9 +5703,10 @@ async function applyGroupChange({ // modifyDescription?: GroupChange.Actions.ModifyDescriptionAction; if (actions.modifyDescription) { - const { description } = actions.modifyDescription; - if (description && description.content === 'descriptionText') { - result.description = dropNull(description.descriptionText)?.trim(); + const description = + actions.modifyDescription.description?.content?.descriptionText; + if (description != null) { + result.description = description.trim(); } else { log.warn( `applyGroupChange/${logId}: Clearing group description due to missing data.` @@ -5774,18 +5775,14 @@ export async function decryptGroupAvatar( const clientZkGroupCipher = getClientZkGroupCipher(secretParamsBase64); const plaintext = decryptGroupBlob(clientZkGroupCipher, ciphertext); const blob = Proto.GroupAttributeBlob.decode(plaintext); - if (blob.content !== 'avatar') { + + if (blob.content?.avatar == null) { throw new Error( `decryptGroupAvatar: Returned blob had incorrect content: ${blob.content}` ); } - const avatar = dropNull(blob.avatar); - if (!avatar) { - throw new Error('decryptGroupAvatar: Returned blob had no avatar set!'); - } - - return avatar; + return blob.content.avatar; } // Overwriting result.avatar as part of functionality @@ -5919,8 +5916,6 @@ async function applyGroupState({ sourceServiceId?: ServiceIdString; }): Promise { const logId = idForLogging(group.groupId); - const ACCESS_ENUM = Proto.AccessControl.AccessRequired; - const MEMBER_ROLE_ENUM = Proto.Member.Role; const version = groupState.version || 0; let result = { ...group }; const newProfileKeys: Array = []; @@ -5950,23 +5945,21 @@ async function applyGroupState({ // title // Note: During decryption, title becomes a GroupAttributeBlob - const { title } = groupState; - if (title && title.content === 'title') { - result.name = dropNull(title.title)?.trim(); + const title = groupState.title?.content?.title; + if (title != null) { + result.name = title.trim(); } else { result.name = undefined; } // disappearingMessagesTimer // Note: during decryption, disappearingMessageTimer becomes a GroupAttributeBlob - const { disappearingMessagesTimer } = groupState; - if ( - disappearingMessagesTimer && - disappearingMessagesTimer.content === 'disappearingMessagesDuration' - ) { - const duration = disappearingMessagesTimer.disappearingMessagesDuration; - result.expireTimer = - duration == null ? undefined : DurationInSeconds.fromSeconds(duration); + const disappearingMessagesDuration = + groupState.disappearingMessagesTimer?.content?.disappearingMessagesDuration; + if (disappearingMessagesDuration != null) { + result.expireTimer = DurationInSeconds.fromSeconds( + disappearingMessagesDuration + ); } else { result.expireTimer = undefined; } @@ -5975,13 +5968,13 @@ async function applyGroupState({ const { accessControl } = groupState; result.accessControl = { attributes: - (accessControl && accessControl.attributes) || ACCESS_ENUM.MEMBER, - members: (accessControl && accessControl.members) || ACCESS_ENUM.MEMBER, + (accessControl && accessControl.attributes) || AccessRequired.MEMBER, + members: (accessControl && accessControl.members) || AccessRequired.MEMBER, addFromInviteLink: (accessControl && accessControl.addFromInviteLink) || - ACCESS_ENUM.UNSATISFIABLE, + AccessRequired.UNSATISFIABLE, memberLabel: - (accessControl && accessControl.memberLabel) || ACCESS_ENUM.MEMBER, + (accessControl && accessControl.memberLabel) || AccessRequired.MEMBER, }; // Optimization: we assume we have left the group unless we are found in members @@ -6046,7 +6039,7 @@ async function applyGroupState({ // Note: role changes will be reflected in group update messages return { - role: member.role || MEMBER_ROLE_ENUM.DEFAULT, + role: member.role || MemberRole.DEFAULT, joinedAtVersion: member.joinedAtVersion, aci: member.userId, labelEmoji: member.labelEmoji, @@ -6106,7 +6099,7 @@ async function applyGroupState({ addedByUserId: member.addedByUserId, serviceId: member.member.userId, timestamp: member.timestamp, - role: member.member.role || MEMBER_ROLE_ENUM.DEFAULT, + role: member.member.role || MemberRole.DEFAULT, }; } ); @@ -6158,9 +6151,9 @@ async function applyGroupState({ } // description - const { description } = groupState; - if (description && description.content === 'descriptionText') { - result.description = dropNull(description.descriptionText)?.trim(); + const description = groupState.description?.content?.descriptionText; + if (description != null) { + result.description = description.trim(); } else { result.description = undefined; } @@ -6210,28 +6203,34 @@ async function applyGroupState({ }; } -function isValidRole(role?: number): role is number { - const MEMBER_ROLE_ENUM = Proto.Member.Role; +function isValidRole( + role?: Proto.Member.Params['role'] +): role is MemberRole.ADMINISTRATOR | MemberRole.DEFAULT { + return role === MemberRole.ADMINISTRATOR || role === MemberRole.DEFAULT; +} +function isValidAccess( + access?: Proto.AccessControl.Params['attributes'] +): access is AccessRequired.ADMINISTRATOR | AccessRequired.MEMBER { + // TODO(DESKTOP-9868) return ( - role === MEMBER_ROLE_ENUM.ADMINISTRATOR || role === MEMBER_ROLE_ENUM.DEFAULT + access === AccessRequired.ADMINISTRATOR || access === AccessRequired.MEMBER ); } -function isValidAccess(access?: number): access is number { - const ACCESS_ENUM = Proto.AccessControl.AccessRequired; - - return access === ACCESS_ENUM.ADMINISTRATOR || access === ACCESS_ENUM.MEMBER; -} - -function isValidLinkAccess(access?: number): access is number { - const ACCESS_ENUM = Proto.AccessControl.AccessRequired; - +function isValidLinkAccess( + access?: Proto.AccessControl.Params['addFromInviteLink'] +): access is + | AccessRequired.UNKNOWN + | AccessRequired.ANY + | AccessRequired.ADMINISTRATOR + | AccessRequired.UNSATISFIABLE { + // TODO(DESKTOP-9868) return ( - access === ACCESS_ENUM.UNKNOWN || - access === ACCESS_ENUM.ANY || - access === ACCESS_ENUM.ADMINISTRATOR || - access === ACCESS_ENUM.UNSATISFIABLE + access === AccessRequired.UNKNOWN || + access === AccessRequired.ANY || + access === AccessRequired.ADMINISTRATOR || + access === AccessRequired.UNSATISFIABLE ); } @@ -6239,12 +6238,12 @@ function isValidProfileKey(buffer?: Uint8Array): boolean { return Boolean(buffer && buffer.length === 32); } -function normalizeTimestamp(timestamp: Long | null | undefined): number { +function normalizeTimestamp(timestamp: bigint | null | undefined): number { if (!timestamp) { return 0; } - const asNumber = timestamp.toNumber(); + const asNumber = toNumber(timestamp); const now = Date.now(); if (!asNumber || asNumber > now) { @@ -6272,7 +6271,7 @@ type DecryptedGroupChangeActions = { }>; modifyMemberRoles?: ReadonlyArray<{ userId: AciString; - role: Proto.Member.Role; + role: MemberRole; }>; modifyMemberLabels?: ReadonlyArray; modifyMemberProfileKeys?: ReadonlyArray<{ @@ -6309,7 +6308,7 @@ type DecryptedGroupChangeActions = { }>; promoteMembersPendingAdminApproval?: ReadonlyArray<{ userId: AciString; - role: Proto.Member.Role; + role: MemberRole; }>; modifyInviteLinkPassword?: { inviteLinkPassword?: string; @@ -6323,17 +6322,25 @@ type DecryptedGroupChangeActions = { addMembersBanned?: ReadonlyArray; // This might be a PNI deleteMembersBanned?: ReadonlyArray; -} & Pick< - Proto.GroupChange.IActions, - | 'modifyAttributesAccess' - | 'modifyMemberAccess' - | 'modifyAddFromInviteLinkAccess' - | 'modifyMemberLabelAccess' - | 'modifyAvatar' ->; + modifyAttributesAccess?: { + attributesAccess: AccessRequired; + }; + modifyMemberAccess?: { + membersAccess: AccessRequired; + }; + modifyAddFromInviteLinkAccess?: { + addFromInviteLinkAccess: AccessRequired; + }; + modifyMemberLabelAccess?: { + memberLabelAccess: AccessRequired; + }; + modifyAvatar?: { + avatar: string; + }; +}; function decryptGroupChange( - actions: Readonly, + actions: Readonly, groupSecretParams: string, logId: string ): DecryptedGroupChangeActions { @@ -6433,7 +6440,7 @@ function decryptGroupChange( return null; } - const role = dropNull(modifyMember.role); + const { role } = modifyMember; if (!isValidRole(role)) { throw new Error( `decryptGroupChange: modifyMemberRole had invalid role ${modifyMember.role}` @@ -6726,7 +6733,11 @@ function decryptGroupChange( // modifyAvatar?: GroupChange.Actions.ModifyAvatarAction; // Note: decryption happens during application of the change, on download of the avatar - result.modifyAvatar = actions.modifyAvatar; + if (actions.modifyAvatar != null) { + result.modifyAvatar = { + avatar: actions.modifyAvatar.avatar, + }; + } // modifyDisappearingMessageTimer?: // GroupChange.Actions.ModifyDisappearingMessageTimerAction; @@ -6754,9 +6765,7 @@ function decryptGroupChange( // modifyAttributesAccess?: // GroupChange.Actions.ModifyAttributesAccessControlAction; if (actions.modifyAttributesAccess) { - const attributesAccess = dropNull( - actions.modifyAttributesAccess.attributesAccess - ); + const { attributesAccess } = actions.modifyAttributesAccess; strictAssert( isValidAccess(attributesAccess), `decryptGroupChange: modifyAttributesAccess.attributesAccess was not valid: ${actions.modifyAttributesAccess.attributesAccess}` @@ -6769,7 +6778,7 @@ function decryptGroupChange( // modifyMemberAccess?: GroupChange.Actions.ModifyMembersAccessControlAction; if (actions.modifyMemberAccess) { - const membersAccess = dropNull(actions.modifyMemberAccess.membersAccess); + const { membersAccess } = actions.modifyMemberAccess; strictAssert( isValidAccess(membersAccess), `decryptGroupChange: modifyMemberAccess.membersAccess was not valid: ${actions.modifyMemberAccess.membersAccess}` @@ -6783,9 +6792,7 @@ function decryptGroupChange( // modifyAddFromInviteLinkAccess?: // GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction; if (actions.modifyAddFromInviteLinkAccess) { - const addFromInviteLinkAccess = dropNull( - actions.modifyAddFromInviteLinkAccess.addFromInviteLinkAccess - ); + const { addFromInviteLinkAccess } = actions.modifyAddFromInviteLinkAccess; strictAssert( isValidLinkAccess(addFromInviteLinkAccess), `decryptGroupChange: modifyAddFromInviteLinkAccess.addFromInviteLinkAccess was not valid: ${actions.modifyAddFromInviteLinkAccess.addFromInviteLinkAccess}` @@ -6798,9 +6805,7 @@ function decryptGroupChange( // modifyMemberLabelAccess?: GroupChange.Actions.ModifyMemberLabelAccessControlAction; if (actions.modifyMemberLabelAccess) { - const memberLabelAccess = dropNull( - actions.modifyMemberLabelAccess.memberLabelAccess - ); + const { memberLabelAccess } = actions.modifyMemberLabelAccess; strictAssert( isValidAccess(memberLabelAccess), `decryptGroupChange: modifyMemberLabelAccess.memberLabelAccess was not valid: ${actions.modifyMemberLabelAccess.memberLabelAccess}` @@ -6891,7 +6896,7 @@ function decryptGroupChange( return null; } - const role = dropNull(promoteMemberPendingAdminApproval.role); + const { role } = promoteMemberPendingAdminApproval; if (!isValidRole(role)) { throw new Error( `decryptGroupChange: promoteMemberPendingAdminApproval had invalid role ${promoteMemberPendingAdminApproval.role}` @@ -6995,11 +7000,7 @@ export function decryptGroupTitle( decryptGroupBlob(clientZkGroupCipher, title) ); - if (blob && blob.content === 'title') { - return dropNull(blob.title); - } - - return undefined; + return blob.content?.title; } export function decryptGroupDescription( @@ -7015,16 +7016,12 @@ export function decryptGroupDescription( decryptGroupBlob(clientZkGroupCipher, description) ); - if (blob && blob.content === 'descriptionText') { - return dropNull(blob.descriptionText); - } - - return undefined; + return blob?.content?.descriptionText; } function decryptModifyMemberLabelAction( clientZkGroupCipher: ClientZkGroupCipher, - modifyMember: Readonly, + modifyMember: Readonly, logId: string ): DecryptedModifyMemberLabelAction | undefined { const { userId, labelEmoji, labelString } = modifyMember; @@ -7104,7 +7101,7 @@ type DecryptedGroupState = { }; function decryptGroupState( - groupState: Readonly, + groupState: Readonly, groupSecretParams: string, logId: string ): DecryptedGroupState { @@ -7175,6 +7172,10 @@ function decryptGroupState( isValidLinkAccess(addFromInviteLink), `decryptGroupState: Access control for invite link is invalid: ${addFromInviteLink}` ); + strictAssert( + isValidAccess(memberLabel), + `decryptGroupState: Access control for member label is invalid: ${memberLabel}` + ); result.accessControl = { attributes, @@ -7195,7 +7196,7 @@ function decryptGroupState( // members if (groupState.members) { result.members = compact( - groupState.members.map((member: Proto.IMember) => + groupState.members.map((member: Proto.Member.Params) => decryptMember(clientZkGroupCipher, member, logId) ) ); @@ -7205,7 +7206,7 @@ function decryptGroupState( if (groupState.membersPendingProfileKey) { result.membersPendingProfileKey = compact( groupState.membersPendingProfileKey.map( - (member: Proto.IMemberPendingProfileKey) => + (member: Proto.MemberPendingProfileKey.Params) => decryptMemberPendingProfileKey(clientZkGroupCipher, member, logId) ) ); @@ -7215,7 +7216,7 @@ function decryptGroupState( if (groupState.membersPendingAdminApproval) { result.membersPendingAdminApproval = compact( groupState.membersPendingAdminApproval.map( - (member: Proto.IMemberPendingAdminApproval) => + (member: Proto.MemberPendingAdminApproval.Params) => decryptMemberPendingAdminApproval(clientZkGroupCipher, member, logId) ) ); @@ -7256,7 +7257,7 @@ function decryptGroupState( return null; } const serviceId = decryptServiceId(clientZkGroupCipher, item.userId); - const timestamp = item.timestamp?.toNumber() ?? 0; + const timestamp = toNumber(item.timestamp) ?? 0; return { serviceId, timestamp }; }) @@ -7273,7 +7274,7 @@ function decryptGroupState( type DecryptedMember = Readonly<{ userId: AciString; profileKey: Uint8Array; - role: Proto.Member.Role; + role: MemberRole; joinedAtVersion: number; labelEmoji?: string; labelString?: string; @@ -7281,7 +7282,7 @@ type DecryptedMember = Readonly<{ function decryptMember( clientZkGroupCipher: ClientZkGroupCipher, - member: Readonly, + member: Readonly, logId: string ): DecryptedMember | undefined { // userId @@ -7317,10 +7318,10 @@ function decryptMember( } // role - const role = dropNull(member.role); + const { role } = member; if (!isValidRole(role)) { - throw new Error(`decryptMember: Member had invalid role ${member.role}`); + throw new Error(`decryptMember: Member had invalid role ${role}`); } // labelEmoji @@ -7368,13 +7369,13 @@ type DecryptedMemberPendingProfileKey = { timestamp: number; member: { userId: ServiceIdString; - role?: Proto.Member.Role; + role?: MemberRole; }; }; function decryptMemberPendingProfileKey( clientZkGroupCipher: ClientZkGroupCipher, - member: Readonly, + member: Readonly, logId: string ): DecryptedMemberPendingProfileKey | undefined { // addedByUserId @@ -7429,7 +7430,7 @@ function decryptMemberPendingProfileKey( } // role - const role = dropNull(member.member.role); + const { role } = member.member; strictAssert( isValidRole(role), @@ -7454,7 +7455,7 @@ type DecryptedMemberPendingAdminApproval = { function decryptMemberPendingAdminApproval( clientZkGroupCipher: ClientZkGroupCipher, - member: Readonly, + member: Readonly, logId: string ): DecryptedMemberPendingAdminApproval | undefined { // timestamp diff --git a/ts/groups/joinViaLink.preload.ts b/ts/groups/joinViaLink.preload.ts index d8223626b6..ebd027ed7f 100644 --- a/ts/groups/joinViaLink.preload.ts +++ b/ts/groups/joinViaLink.preload.ts @@ -123,7 +123,7 @@ export async function joinViaLink(value: string): Promise { return; } - if (!isAccessControlEnabled(dropNull(result.addFromInviteLink))) { + if (!isAccessControlEnabled(result.addFromInviteLink)) { log.error( `${logId}: addFromInviteLink value of ${result.addFromInviteLink} is invalid` ); diff --git a/ts/groups/util.std.ts b/ts/groups/util.std.ts index dd923f4d0a..30929a8478 100644 --- a/ts/groups/util.std.ts +++ b/ts/groups/util.std.ts @@ -3,13 +3,14 @@ import { SignalService as Proto } from '../protobuf/index.std.js'; -const ACCESS_ENUM = Proto.AccessControl.AccessRequired; +import AccessRequired = Proto.AccessControl.AccessRequired; +// TODO(DESKTOP-9868) export function isAccessControlEnabled( - accessControl: number | undefined -): boolean { + accessControl?: Proto.AccessControl.Params['attributes'] +): accessControl is AccessRequired.ANY | AccessRequired.ADMINISTRATOR { return ( - accessControl === ACCESS_ENUM.ANY || - accessControl === ACCESS_ENUM.ADMINISTRATOR + accessControl === AccessRequired.ANY || + accessControl === AccessRequired.ADMINISTRATOR ); } diff --git a/ts/jobs/helpers/sendCallingMessage.preload.ts b/ts/jobs/helpers/sendCallingMessage.preload.ts index e0698ef156..a73d0a3fcf 100644 --- a/ts/jobs/helpers/sendCallingMessage.preload.ts +++ b/ts/jobs/helpers/sendCallingMessage.preload.ts @@ -99,7 +99,11 @@ export async function sendCallingMessage( await handleMessageSend( sendContentMessageToGroup({ contentHint: ContentHint.Default, - contentMessage: new Proto.Content({ callMessage }), + contentMessage: { + content: { callMessage }, + senderKeyDistributionMessage: null, + pniSignatureMessage: null, + }, isPartialSend, messageId: undefined, recipients, diff --git a/ts/jobs/helpers/sendDeleteForEveryone.preload.ts b/ts/jobs/helpers/sendDeleteForEveryone.preload.ts index 6a1c0ac67e..9d9d74b2f2 100644 --- a/ts/jobs/helpers/sendDeleteForEveryone.preload.ts +++ b/ts/jobs/helpers/sendDeleteForEveryone.preload.ts @@ -155,15 +155,15 @@ export async function sendDeleteForEveryone( expireTimerVersion: undefined, }); strictAssert( - proto.dataMessage, + proto.content?.dataMessage, 'ContentMessage must have dataMessage' ); await handleMessageSend( messaging.sendSyncMessage({ encodedDataMessage: Proto.DataMessage.encode( - proto.dataMessage - ).finish(), + proto.content.dataMessage + ), destinationE164: conversation.get('e164'), destinationServiceId: conversation.getServiceId(), expirationStartTimestamp: null, diff --git a/ts/jobs/helpers/sendDeleteStoryForEveryone.preload.ts b/ts/jobs/helpers/sendDeleteStoryForEveryone.preload.ts index 7e7c8c7f7b..e035726493 100644 --- a/ts/jobs/helpers/sendDeleteStoryForEveryone.preload.ts +++ b/ts/jobs/helpers/sendDeleteStoryForEveryone.preload.ts @@ -29,6 +29,7 @@ import type { MessageModel } from '../../models/messages.preload.js'; import { SendMessageProtoError } from '../../textsecure/Errors.std.js'; import { strictAssert } from '../../util/assert.std.js'; import type { LoggerType } from '../../types/Logging.std.js'; +import { normalizeStoryDistributionId } from '../../types/StoryDistributionId.std.js'; import { isStory } from '../../messages/helpers.std.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js'; @@ -221,10 +222,21 @@ export async function sendDeleteStoryForEveryone( destinationE164: undefined, destinationServiceId, storyMessageRecipients: updatedStoryRecipients?.map( - ({ destinationServiceId: legacyDestinationUuid, ...rest }) => { + ({ + destinationServiceId: legacyDestinationUuid, + distributionListIds, + ...rest + }) => { return { // The field was renamed. legacyDestinationUuid, + distributionListIds: distributionListIds?.map(id => { + return normalizeStoryDistributionId( + id, + 'sendDeleteStoryForEveryone', + log + ); + }), ...rest, }; } diff --git a/ts/jobs/helpers/sendDirectExpirationTimerUpdate.preload.ts b/ts/jobs/helpers/sendDirectExpirationTimerUpdate.preload.ts index a1e1ed0384..a0cc8fe946 100644 --- a/ts/jobs/helpers/sendDirectExpirationTimerUpdate.preload.ts +++ b/ts/jobs/helpers/sendDirectExpirationTimerUpdate.preload.ts @@ -92,7 +92,7 @@ export async function sendDirectExpirationTimerUpdate( includePniSignatureMessage: true, }); - if (!proto.dataMessage) { + if (!proto.content?.dataMessage) { log.error( "ContentMessage proto didn't have a data message; canceling job." ); @@ -106,8 +106,8 @@ export async function sendDirectExpirationTimerUpdate( await handleMessageSend( messaging.sendSyncMessage({ encodedDataMessage: Proto.DataMessage.encode( - proto.dataMessage - ).finish(), + proto.content.dataMessage + ), destinationE164: conversation.get('e164'), destinationServiceId: conversation.getServiceId(), expirationStartTimestamp: null, diff --git a/ts/jobs/helpers/sendNormalMessage.preload.ts b/ts/jobs/helpers/sendNormalMessage.preload.ts index 1a5a552f90..ddabbfc051 100644 --- a/ts/jobs/helpers/sendNormalMessage.preload.ts +++ b/ts/jobs/helpers/sendNormalMessage.preload.ts @@ -592,7 +592,7 @@ async function getMessageSendData({ message: MessageModel; targetTimestamp: number; }>): Promise<{ - attachments: Array; + attachments: Array; body: undefined | string; contact?: Array; expireTimer: undefined | DurationInSeconds; diff --git a/ts/jobs/helpers/sendPollTerminate.preload.ts b/ts/jobs/helpers/sendPollTerminate.preload.ts index ad040e2fc0..3dec46e289 100644 --- a/ts/jobs/helpers/sendPollTerminate.preload.ts +++ b/ts/jobs/helpers/sendPollTerminate.preload.ts @@ -118,7 +118,7 @@ export async function sendPollTerminate( await handleMessageSend( messaging.sendSyncMessage({ - encodedDataMessage: Proto.DataMessage.encode(dataMessage).finish(), + encodedDataMessage: Proto.DataMessage.encode(dataMessage), destinationE164: conversation.get('e164'), destinationServiceId: conversation.getServiceId(), expirationStartTimestamp: null, diff --git a/ts/jobs/helpers/sendStory.preload.ts b/ts/jobs/helpers/sendStory.preload.ts index 68d6d69801..cd1f14f062 100644 --- a/ts/jobs/helpers/sendStory.preload.ts +++ b/ts/jobs/helpers/sendStory.preload.ts @@ -26,7 +26,7 @@ import type { StoryDistributionIdString } from '../../types/StoryDistributionId. import * as Errors from '../../types/errors.std.js'; import type { StoryMessageRecipientsType } from '../../types/Stories.std.js'; import { DataReader } from '../../sql/Client.preload.js'; -import { SignalService as Proto } from '../../protobuf/index.std.js'; +import type { SignalService as Proto } from '../../protobuf/index.std.js'; import { getMessagesById } from '../../messages/getMessagesById.preload.js'; import { getSendOptions, @@ -132,7 +132,7 @@ export async function sendStory( // can reuse it but first we'll need textAttachment | fileAttachment. // This function pulls off the attachment and generates the proto from the // first message on the list prior to continuing. - let originalStoryMessage: Proto.StoryMessage; + let originalStoryMessage: Proto.StoryMessage.Params; { const [originalMessageId] = messageIds; const originalMessage = messages.find( @@ -339,15 +339,15 @@ export async function sendStory( `stories.sendStory(${timestamp}): sending story to ${receiverId}` ); - const storyMessage = new Proto.StoryMessage(); - storyMessage.bodyRanges = originalStoryMessage.bodyRanges; - storyMessage.profileKey = originalStoryMessage.profileKey; - storyMessage.fileAttachment = originalStoryMessage.fileAttachment; - storyMessage.textAttachment = originalStoryMessage.textAttachment; - storyMessage.group = originalStoryMessage.group; - storyMessage.allowsReplies = - isGroupV2(conversation.attributes) || - Boolean(distributionList?.allowsReplies); + const storyMessage: Proto.StoryMessage.Params = { + bodyRanges: originalStoryMessage.bodyRanges, + profileKey: originalStoryMessage.profileKey, + attachment: originalStoryMessage.attachment, + group: originalStoryMessage.group, + allowsReplies: + isGroupV2(conversation.attributes) || + Boolean(distributionList?.allowsReplies), + }; const sendTarget = distributionList ? distributionListToSendTarget( @@ -356,12 +356,15 @@ export async function sendStory( ) : conversation.toSenderKeyTarget(); - const contentMessage = new Proto.Content(); - contentMessage.storyMessage = storyMessage; - const innerPromise = sendContentMessageToGroup({ contentHint: ContentHint.Implicit, - contentMessage, + contentMessage: { + content: { + storyMessage, + }, + senderKeyDistributionMessage: null, + pniSignatureMessage: null, + }, isPartialSend: false, messageId: undefined, recipients: pendingSendRecipientServiceIds, diff --git a/ts/models/conversations.preload.ts b/ts/models/conversations.preload.ts index e1d5dda5a6..5d35f8a1f0 100644 --- a/ts/models/conversations.preload.ts +++ b/ts/models/conversations.preload.ts @@ -588,7 +588,7 @@ export class ConversationModel { async updateExpirationTimerInGroupV2( seconds?: DurationInSeconds - ): Promise { + ): Promise { const idLog = this.idForLogging(); const current = this.get('expireTimer'); const bothFalsey = Boolean(current) === false && Boolean(seconds) === false; @@ -608,7 +608,7 @@ export class ConversationModel { async #promotePendingMember( serviceIdKind: ServiceIdKind - ): Promise { + ): Promise { const idLog = this.idForLogging(); const us = window.ConversationController.getOurConversationOrThrow(); @@ -658,7 +658,7 @@ export class ConversationModel { async #denyPendingApprovalRequest( aci: AciString - ): Promise { + ): Promise { const idLog = this.idForLogging(); // This user's pending state may have changed in the time between the user's @@ -682,7 +682,7 @@ export class ConversationModel { } async addPendingApprovalRequest(): Promise< - Proto.GroupChange.Actions | undefined + Proto.GroupChange.Actions.Params | undefined > { const idLog = this.idForLogging(); @@ -727,7 +727,7 @@ export class ConversationModel { async addMember( serviceId: ServiceIdString - ): Promise { + ): Promise { const idLog = this.idForLogging(); const toRequest = window.ConversationController.get(serviceId); @@ -773,13 +773,13 @@ export class ConversationModel { async #removePendingMember( serviceIds: ReadonlyArray - ): Promise { + ): Promise { return removePendingMember(this.attributes, serviceIds); } async #removeMember( serviceId: ServiceIdString - ): Promise { + ): Promise { const idLog = this.idForLogging(); // This user's pending state may have changed in the time between the user's @@ -803,7 +803,7 @@ export class ConversationModel { async #toggleAdminChange( serviceId: ServiceIdString - ): Promise { + ): Promise { if (!isGroupV2(this.attributes)) { return undefined; } @@ -839,7 +839,9 @@ export class ConversationModel { syncMessageOnly, }: { usingCredentialsFrom: ReadonlyArray; - createGroupChange: () => Promise; + createGroupChange: () => Promise< + Proto.GroupChange.Actions.Params | undefined + >; extraConversationsForSend?: ReadonlyArray; inviteLinkPassword?: string; name: string; @@ -2843,7 +2845,7 @@ export class ConversationModel { async addBannedMember( serviceId: ServiceIdString - ): Promise { + ): Promise { if (this.isMember(serviceId)) { log.warn('addBannedMember: Member is a part of the group!'); diff --git a/ts/protobuf/index.std.ts b/ts/protobuf/index.std.ts index 7433fe9911..e3537779cc 100644 --- a/ts/protobuf/index.std.ts +++ b/ts/protobuf/index.std.ts @@ -1,8 +1,6 @@ // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import './wrap.std.js'; - import { signal as Signal, signalbackups as Backups, diff --git a/ts/protobuf/wrap.std.ts b/ts/protobuf/wrap.std.ts deleted file mode 100644 index 0c9498d635..0000000000 --- a/ts/protobuf/wrap.std.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2022 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as protobuf from 'protobufjs/minimal'; -import Long from 'long'; - -protobuf.util.Long = Long; -protobuf.configure(); - -export default protobuf; diff --git a/ts/services/backups/constants.std.ts b/ts/services/backups/constants.std.ts index 9a8bcd4936..6944ea38ec 100644 --- a/ts/services/backups/constants.std.ts +++ b/ts/services/backups/constants.std.ts @@ -4,7 +4,7 @@ import { Backups } from '../../protobuf/index.std.js'; import type { ConversationColorType } from '../../types/Colors.std.js'; -export const BACKUP_VERSION = 1; +export const BACKUP_VERSION = 1n; export const LOCAL_BACKUP_VERSION = 1; diff --git a/ts/services/backups/errors.std.ts b/ts/services/backups/errors.std.ts index bebbef54b9..10dea30bf1 100644 --- a/ts/services/backups/errors.std.ts +++ b/ts/services/backups/errors.std.ts @@ -2,8 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only /* eslint-disable max-classes-per-file */ -import type Long from 'long'; - import { InstallScreenBackupError } from '../../types/InstallScreen.std.js'; export class BackupInstallerError extends Error { @@ -16,7 +14,7 @@ export class BackupInstallerError extends Error { } export class UnsupportedBackupVersion extends BackupInstallerError { - constructor(version: Long) { + constructor(version: bigint) { super( `Unsupported backup version: ${version}`, InstallScreenBackupError.UnsupportedVersion diff --git a/ts/services/backups/export.preload.ts b/ts/services/backups/export.preload.ts index 1d9d133297..8b2a8b427d 100644 --- a/ts/services/backups/export.preload.ts +++ b/ts/services/backups/export.preload.ts @@ -1,7 +1,6 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import Long from 'long'; import { Aci, Pni, ServiceId } from '@signalapp/libsignal-client'; import { BackupJsonExporter } from '@signalapp/libsignal-client/dist/MessageBackup.js'; import pMap from 'p-map'; @@ -186,6 +185,8 @@ import { ChatFolderType } from '../../types/ChatFolder.std.js'; import { expiresTooSoonForBackup } from './util/expiration.std.js'; import type { PinnedMessage } from '../../types/PinnedMessage.std.js'; import type { ThemeType } from '../../util/preload.preload.js'; +import { MAX_VALUE as LONG_MAX_VALUE } from '../../util/long.std.js'; +import { encodeDelimited } from '../../util/encodeDelimited.std.js'; import { safeParseStrict } from '../../util/schemas.std.js'; const { isNumber } = lodash; @@ -238,7 +239,7 @@ type NonBubbleOptionsType = Pick< 'aboutMe' | 'callHistoryByCallId' > & Readonly<{ - authorId: Long | undefined; + authorId: bigint | undefined; message: MessageAttributesType; }>; @@ -255,7 +256,10 @@ type NonBubbleResultType = Readonly< } | { kind: NonBubbleResultKind.Directed | NonBubbleResultKind.Directionless; - patch: Backups.IChatItem; + patch: { + item: Backups.ChatItem.Params['item']; + authorId?: bigint; + }; } >; @@ -264,11 +268,14 @@ export class BackupExportStream extends Readable { #now = Date.now(); readonly #backupTimeMs = getSafeLongFromTimestamp(this.#now); - readonly #convoIdToRecipientId = new Map(); - readonly #serviceIdToRecipientId = new Map(); - readonly #e164ToRecipientId = new Map(); - readonly #roomIdToRecipientId = new Map(); - readonly #mediaNamesToFilePointers = new Map(); + readonly #convoIdToRecipientId = new Map(); + readonly #serviceIdToRecipientId = new Map(); + readonly #e164ToRecipientId = new Map(); + readonly #roomIdToRecipientId = new Map(); + readonly #mediaNamesToFilePointers = new Map< + string, + Backups.FilePointer.Params + >(); readonly #stats: StatsType = { adHocCalls: 0, callLinks: 0, @@ -287,13 +294,13 @@ export class BackupExportStream extends Readable { CoreAttachmentBackupJobType | CoreAttachmentLocalBackupJobType > = []; #buffers = new Array(); - #nextRecipientId = 1; + #nextRecipientId = 1n; #flushResolve: (() => void) | undefined; #jsonExporter: BackupJsonExporter | undefined; // Map from custom color uuid to an index in accountSettings.customColors // array. - #customColorIdByUuid = new Map(); + #customColorIdByUuid = new Map(); constructor( private readonly options: Readonly & { @@ -389,24 +396,29 @@ export class BackupExportStream extends Readable { async #unsafeRun(): Promise { this.#ourConversation = window.ConversationController.getOurConversationOrThrow().attributes; - const backupInfo: Backups.IBackupInfo = { - version: Long.fromNumber(BACKUP_VERSION), + const backupInfo: Backups.BackupInfo.Params = { + version: BACKUP_VERSION, backupTimeMs: this.#backupTimeMs, mediaRootBackupKey: getBackupMediaRootKey().serialize(), - firstAppVersion: itemStorage.get('restoredBackupFirstAppVersion'), + firstAppVersion: itemStorage.get('restoredBackupFirstAppVersion') ?? null, currentAppVersion: `Desktop ${window.getVersion()}`, + debugInfo: null, }; if (this.options.type === 'plaintext-export') { const { exporter, chunk: initialChunk } = BackupJsonExporter.start( - Backups.BackupInfo.encode(backupInfo).finish(), + Backups.BackupInfo.encode(backupInfo), { validate: false } ); this.#jsonExporter = exporter; this.push(`${initialChunk}\n`); } else { - this.push(Backups.BackupInfo.encodeDelimited(backupInfo).finish()); + for (const chunk of encodeDelimited( + Backups.BackupInfo.encode(backupInfo) + )) { + this.push(chunk); + } } this.#pushFrame({ @@ -443,7 +455,7 @@ export class BackupExportStream extends Readable { identityKeysById, keyTransparencyData, }); - if (recipient === undefined) { + if (recipient == null) { skippedConversationIds.add(attributes.id); // Can't be backed up. continue; @@ -460,8 +472,8 @@ export class BackupExportStream extends Readable { this.#pushFrame({ recipient: { - id: Long.fromNumber(this.#getNextRecipientId()), - releaseNotes: {}, + id: this.#getNextRecipientId(), + destination: { releaseNotes: {} }, }, }); await this.#flush(); @@ -489,23 +501,25 @@ export class BackupExportStream extends Readable { this.#pushFrame({ recipient: { - id: Long.fromNumber(this.#getNextRecipientId()), - distributionList: { - distributionId: uuidToBytes(list.id), - deletionTimestamp: list.deletedAtTimestamp - ? Long.fromNumber(list.deletedAtTimestamp) - : null, - - distributionList: list.deletedAtTimestamp - ? null - : { - name: list.name, - allowReplies: list.allowsReplies, - privacyMode, - memberRecipientIds: list.members.map(serviceId => - this.#getOrPushPrivateRecipient({ serviceId }) - ), - }, + id: this.#getNextRecipientId(), + destination: { + distributionList: { + distributionId: uuidToBytes(list.id), + item: list.deletedAtTimestamp + ? { + deletionTimestamp: BigInt(list.deletedAtTimestamp), + } + : { + distributionList: { + name: list.name, + allowReplies: list.allowsReplies, + privacyMode, + memberRecipientIds: list.members.map(serviceId => + this.#getOrPushPrivateRecipient({ serviceId }) + ), + }, + }, + }, }, }, }); @@ -539,15 +553,17 @@ export class BackupExportStream extends Readable { this.#pushFrame({ recipient: { - id: Long.fromNumber(id), - callLink: { - rootKey: rootKey.bytes, - adminKey: adminKey ? toAdminKeyBytes(adminKey) : null, - name, - restrictions: toCallLinkRestrictionsProto(restrictions), - expirationMs: isNumber(expiration) - ? getSafeLongFromTimestamp(expiration) - : null, + id, + destination: { + callLink: { + rootKey: rootKey.bytes, + adminKey: adminKey ? toAdminKeyBytes(adminKey) : null, + name, + restrictions: toCallLinkRestrictionsProto(restrictions), + expirationMs: isNumber(expiration) + ? getSafeLongFromTimestamp(expiration) + : null, + }, }, }, }); @@ -621,13 +637,11 @@ export class BackupExportStream extends Readable { pinnedOrder, expirationTimerMs: attributes.expireTimer != null - ? Long.fromNumber( - DurationInSeconds.toMillis(attributes.expireTimer) - ) + ? BigInt(DurationInSeconds.toMillis(attributes.expireTimer)) : null, expireTimerVersion: attributes.expireTimerVersion, muteUntilMs: attributes.muteExpiresAt - ? getSafeLongFromTimestamp(attributes.muteExpiresAt, Long.MAX_VALUE) + ? getSafeLongFromTimestamp(attributes.muteExpiresAt, LONG_MAX_VALUE) : null, markedUnread: attributes.markedUnread === true, dontNotifyForMentionsIfMuted: @@ -667,7 +681,7 @@ export class BackupExportStream extends Readable { } const recipientId = this.#roomIdToRecipientId.get(roomId); - if (!recipientId) { + if (recipientId == null) { log.warn( `Dropping ad-hoc call; recipientId for roomId ${roomId.slice(-2)} not found` ); @@ -678,9 +692,9 @@ export class BackupExportStream extends Readable { continue; } - let callId: Long; + let callId: bigint; try { - callId = Long.fromString(callIdStr); + callId = BigInt(callIdStr); } catch (error) { log.warn('Dropping ad-hoc call; invalid callId', toLogFormat(error)); continue; @@ -689,9 +703,9 @@ export class BackupExportStream extends Readable { this.#pushFrame({ adHocCall: { callId, - recipientId: Long.fromNumber(recipientId), + recipientId, state: toAdHocCallStateProto(status), - callTimestamp: Long.fromNumber(timestamp), + callTimestamp: BigInt(timestamp), }, }); @@ -707,15 +721,15 @@ export class BackupExportStream extends Readable { const { id, name, - emoji, + emoji = null, color, createdAtMs, allowAllCalls, allowAllMentions, allowedMembers, scheduleEnabled, - scheduleStartTime, - scheduleEndTime, + scheduleStartTime = null, + scheduleEndTime = null, scheduleDaysEnabled, } = profile; @@ -745,7 +759,7 @@ export class BackupExportStream extends Readable { scheduleEnabled, scheduleStartTime, scheduleEndTime, - scheduleDaysEnabled: toDayOfWeekArray(scheduleDaysEnabled), + scheduleDaysEnabled: toDayOfWeekArray(scheduleDaysEnabled) ?? null, }, }); @@ -885,19 +899,16 @@ export class BackupExportStream extends Readable { this.push(null); } - #pushBuffer(buffer: Uint8Array): void { - this.#buffers.push(buffer); - } - - #pushFrame(frame: Backups.IFrame): void { - const encodedFrame = Backups.Frame.encodeDelimited(frame).finish(); + #pushFrame(frame: Backups.Frame.Params['item']): void { + const encodedFrame = Backups.Frame.encode({ item: frame }); if (this.options.type === 'plaintext-export') { + const delimitedFrame = Buffer.concat(encodeDelimited(encodedFrame)); strictAssert( this.#jsonExporter != null, 'jsonExported must be initialized' ); - const results = this.#jsonExporter.exportFrames(encodedFrame); + const results = this.#jsonExporter.exportFrames(delimitedFrame); for (const result of results) { if (result.errorMessage) { log.warn( @@ -908,11 +919,11 @@ export class BackupExportStream extends Readable { if (!result.line) { log.error('frameToJson: frame was filtered out by libsignal'); } else { - this.#pushBuffer(Buffer.from(`${result.line}\n`)); + this.#buffers.push(Buffer.from(`${result.line}\n`)); } } } else { - this.#pushBuffer(encodedFrame); + this.#buffers.push(...encodeDelimited(encodedFrame)); } } @@ -946,7 +957,7 @@ export class BackupExportStream extends Readable { this.#flushResolve?.(); } - async #toAccountData(): Promise { + async #toAccountData(): Promise { const me = window.ConversationController.getOurConversationOrThrow(); const rawPreferredReactionEmoji = itemStorage.get('preferredReactionEmoji'); @@ -993,19 +1004,19 @@ export class BackupExportStream extends Readable { ); return { - profileKey: itemStorage.get('profileKey'), + profileKey: itemStorage.get('profileKey') ?? null, username: me.get('username') || null, usernameLink: usernameLink ? { ...usernameLink, // Same numeric value, no conversion needed - color: itemStorage.get('usernameLinkColor'), + color: itemStorage.get('usernameLinkColor') ?? null, } : null, - givenName: me.get('profileName'), - familyName: me.get('profileFamilyName'), - avatarUrlPath: itemStorage.get('avatarUrl'), + givenName: me.get('profileName') ?? null, + familyName: me.get('profileFamilyName') ?? null, + avatarUrlPath: itemStorage.get('avatarUrl') ?? null, backupsSubscriberData, donationSubscriberData: Bytes.isNotEmpty(subscriberId) && currencyCode @@ -1018,52 +1029,54 @@ export class BackupExportStream extends Readable { ), } : null, - svrPin: itemStorage.get('svrPin'), - bioText: me.get('about'), - bioEmoji: me.get('aboutEmoji'), - keyTransparencyData, + svrPin: itemStorage.get('svrPin') ?? null, + bioText: me.get('about') ?? null, + bioEmoji: me.get('aboutEmoji') ?? null, + keyTransparencyData: keyTransparencyData ?? null, // Test only values - ...(isTestOrMockEnvironment() - ? { - androidSpecificSettings: itemStorage.get('androidSpecificSettings'), - } - : {}), + androidSpecificSettings: isTestOrMockEnvironment() + ? (itemStorage.get('androidSpecificSettings') ?? null) + : null, accountSettings: { - readReceipts: itemStorage.get('read-receipt-setting'), - sealedSenderIndicators: itemStorage.get('sealedSenderIndicators'), + readReceipts: itemStorage.get('read-receipt-setting') ?? null, + sealedSenderIndicators: + itemStorage.get('sealedSenderIndicators') ?? null, typingIndicators: getTypingIndicatorSetting(), linkPreviews: getLinkPreviewSetting(), notDiscoverableByPhoneNumber: parsePhoneNumberDiscoverability( itemStorage.get('phoneNumberDiscoverability') ) === PhoneNumberDiscoverability.NotDiscoverable, - preferContactAvatars: itemStorage.get('preferContactAvatars'), - universalExpireTimerSeconds: itemStorage.get('universalExpireTimer'), - preferredReactionEmoji, - displayBadgesOnProfile: itemStorage.get('displayBadgesOnProfile'), - keepMutedChatsArchived: itemStorage.get('keepMutedChatsArchived'), - hasSetMyStoriesPrivacy: itemStorage.get('hasSetMyStoriesPrivacy'), - hasViewedOnboardingStory: itemStorage.get('hasViewedOnboardingStory'), - storiesDisabled: itemStorage.get('hasStoriesDisabled'), + preferContactAvatars: itemStorage.get('preferContactAvatars') ?? null, + universalExpireTimerSeconds: + itemStorage.get('universalExpireTimer') ?? null, + preferredReactionEmoji: preferredReactionEmoji ?? null, + displayBadgesOnProfile: + itemStorage.get('displayBadgesOnProfile') ?? null, + keepMutedChatsArchived: + itemStorage.get('keepMutedChatsArchived') ?? null, + hasSetMyStoriesPrivacy: + itemStorage.get('hasSetMyStoriesPrivacy') ?? null, + hasViewedOnboardingStory: + itemStorage.get('hasViewedOnboardingStory') ?? null, + storiesDisabled: itemStorage.get('hasStoriesDisabled') ?? null, allowAutomaticKeyVerification: !itemStorage.get( 'hasKeyTransparencyDisabled' ), - storyViewReceiptsEnabled: itemStorage.get('storyViewReceiptsEnabled'), - hasCompletedUsernameOnboarding: itemStorage.get( - 'hasCompletedUsernameOnboarding' - ), - hasSeenGroupStoryEducationSheet: itemStorage.get( - 'hasSeenGroupStoryEducationSheet' - ), - hasSeenAdminDeleteEducationDialog: itemStorage.get( - 'hasSeenAdminDeleteEducationDialog' - ), + storyViewReceiptsEnabled: + itemStorage.get('storyViewReceiptsEnabled') ?? null, + hasCompletedUsernameOnboarding: + itemStorage.get('hasCompletedUsernameOnboarding') ?? null, + hasSeenGroupStoryEducationSheet: + itemStorage.get('hasSeenGroupStoryEducationSheet') ?? null, + hasSeenAdminDeleteEducationDialog: + itemStorage.get('hasSeenAdminDeleteEducationDialog') ?? null, phoneNumberSharingMode, // Note that this should be called before `toDefaultChatStyle` because // it builds `customColorIdByUuid` customChatColors: this.#toCustomChatColors(), defaultChatStyle: this.#toDefaultChatStyle(), - backupTier: backupTier != null ? Long.fromNumber(backupTier) : null, + backupTier: backupTier != null ? BigInt(backupTier) : null, appTheme, callsUseLessDataSetting: itemStorage.get('callsUseLessDataSetting') || @@ -1072,16 +1085,13 @@ export class BackupExportStream extends Readable { // Test only values ...(isTestOrMockEnvironment() ? { - optimizeOnDeviceStorage: itemStorage.get( - 'optimizeOnDeviceStorage' - ), - allowSealedSenderFromAnyone: itemStorage.get( - 'allowSealedSenderFromAnyone' - ), - pinReminders: itemStorage.get('pinReminders'), - screenLockTimeoutMinutes: itemStorage.get( - 'screenLockTimeoutMinutes' - ), + optimizeOnDeviceStorage: + itemStorage.get('optimizeOnDeviceStorage') ?? null, + allowSealedSenderFromAnyone: + itemStorage.get('allowSealedSenderFromAnyone') ?? null, + pinReminders: itemStorage.get('pinReminders') ?? null, + screenLockTimeoutMinutes: + itemStorage.get('screenLockTimeoutMinutes') ?? null, autoDownloadSettings: autoDownloadPrimary ? { images: autoDownloadPrimary.photos, @@ -1089,9 +1099,15 @@ export class BackupExportStream extends Readable { video: autoDownloadPrimary.videos, documents: autoDownloadPrimary.documents, } - : undefined, + : null, } - : {}), + : { + optimizeOnDeviceStorage: null, + allowSealedSenderFromAnyone: null, + pinReminders: null, + screenLockTimeoutMinutes: null, + autoDownloadSettings: null, + }), defaultSentMediaQuality: itemStorage.get('sent-media-quality') === 'high' ? Backups.AccountData.SentMediaQuality.HIGH @@ -1102,8 +1118,8 @@ export class BackupExportStream extends Readable { #getExistingRecipientId( options: GetRecipientIdOptionsType - ): Long | undefined { - let existing: number | undefined; + ): bigint | undefined { + let existing: bigint | undefined; if (options.serviceId != null) { existing = this.#serviceIdToRecipientId.get(options.serviceId); } @@ -1113,13 +1129,10 @@ export class BackupExportStream extends Readable { if (existing === undefined && options.id != null) { existing = this.#convoIdToRecipientId.get(options.id); } - if (existing !== undefined) { - return Long.fromNumber(existing); - } - return undefined; + return existing; } - #getRecipientId(options: GetRecipientIdOptionsType): Long { + #getRecipientId(options: GetRecipientIdOptionsType): bigint { const existing = this.#getExistingRecipientId(options); if (existing !== undefined) { return existing; @@ -1128,7 +1141,7 @@ export class BackupExportStream extends Readable { const { id, serviceId, e164 } = options; const recipientId = this.#nextRecipientId; - this.#nextRecipientId += 1; + this.#nextRecipientId += 1n; if (id !== undefined) { this.#convoIdToRecipientId.set(id, recipientId); @@ -1139,57 +1152,61 @@ export class BackupExportStream extends Readable { if (e164 !== undefined) { this.#e164ToRecipientId.set(e164, recipientId); } - const result = Long.fromNumber(recipientId); - return result; + return recipientId; } - #getOrPushPrivateRecipient(options: GetRecipientIdOptionsType): Long { + #getOrPushPrivateRecipient(options: GetRecipientIdOptionsType): bigint { const existing = this.#getExistingRecipientId(options); const needsPush = existing == null; const result = this.#getRecipientId(options); if (needsPush) { const { serviceId, e164 } = options; - this.#pushFrame({ - recipient: this.#toRecipient(result, { - type: 'private', - serviceId, - pni: isPniString(serviceId) ? serviceId : undefined, - e164, - }), + const recipient = this.#toRecipient(result, { + type: 'private', + serviceId, + pni: isPniString(serviceId) ? serviceId : undefined, + e164, }); + + if (recipient != null) { + this.#pushFrame({ recipient }); + } } return result; } - #getNextRecipientId(): number { + #getNextRecipientId(): bigint { const recipientId = this.#nextRecipientId; - this.#nextRecipientId += 1; + this.#nextRecipientId += 1n; return recipientId; } #toRecipient( - recipientId: Long, + recipientId: bigint, convo: Omit< ConversationAttributesType, 'id' | 'version' | 'expireTimerVersion' >, options?: ToRecipientOptionsType - ): Backups.IRecipient | undefined { - const res: Backups.IRecipient = { - id: recipientId, - }; - + ): Backups.Recipient.Params | null { if (isMe(convo)) { - res.self = { - avatarColor: toAvatarColor(convo.color), + return { + id: recipientId, + destination: { + self: { + avatarColor: toAvatarColor(convo.color) ?? null, + }, + }, }; - } else if (isDirectConversation(convo)) { + } + + if (isDirectConversation(convo)) { if (convo.serviceId != null && isSignalServiceId(convo.serviceId)) { - return undefined; + return null; } if (convo.serviceId != null && !isServiceIdString(convo.serviceId)) { @@ -1197,17 +1214,17 @@ export class BackupExportStream extends Readable { 'skipping conversation with invalid serviceId', convo.serviceId ); - return undefined; + return null; } if (convo.e164 != null && !isValidE164(convo.e164, true)) { log.warn('skipping conversation with invalid e164', convo.serviceId); - return undefined; + return null; } if (convo.serviceId == null && convo.e164 == null) { log.warn('skipping conversation with neither serviceId nor e164'); - return undefined; + return null; } let visibility: Backups.Contact.Visibility; @@ -1236,59 +1253,65 @@ export class BackupExportStream extends Readable { const pni = isPniString(maybePni) ? Pni.parseFromServiceIdString(maybePni).getRawUuidBytes() : null; - const e164 = convo.e164 ? Long.fromString(convo.e164) : null; + const e164 = convo.e164 ? BigInt(convo.e164) : null; strictAssert( aci != null || pni != null || e164 != null, 'Contact has no identifier' ); - res.contact = { - aci, - pni, - e164, - username: convo.username, - blocked: convo.serviceId - ? itemStorage.blocked.isServiceIdBlocked(convo.serviceId) - : null, - visibility, - ...(convo.discoveredUnregisteredAt - ? { - notRegistered: { - unregisteredTimestamp: convo.firstUnregisteredAt - ? getSafeLongFromTimestamp(convo.firstUnregisteredAt) - : null, - }, - } - : { - registered: {}, - }), - profileKey: convo.profileKey - ? Bytes.fromBase64(convo.profileKey) - : null, - profileSharing: convo.profileSharing, - profileGivenName: convo.profileName, - profileFamilyName: convo.profileFamilyName, - systemFamilyName: convo.systemFamilyName, - systemGivenName: convo.systemGivenName, - systemNickname: convo.systemNickname, - hideStory: convo.hideStory === true, - identityKey: identityKey?.publicKey || null, - avatarColor: toAvatarColor(convo.color), - keyTransparencyData: options?.keyTransparencyData, + return { + id: recipientId, + destination: { + contact: { + aci, + pni, + e164, + username: convo.username ?? null, + blocked: convo.serviceId + ? itemStorage.blocked.isServiceIdBlocked(convo.serviceId) + : null, + visibility, + registration: convo.discoveredUnregisteredAt + ? { + notRegistered: { + unregisteredTimestamp: convo.firstUnregisteredAt + ? getSafeLongFromTimestamp(convo.firstUnregisteredAt) + : null, + }, + } + : { + registered: {}, + }, + profileKey: convo.profileKey + ? Bytes.fromBase64(convo.profileKey) + : null, + profileSharing: convo.profileSharing ?? null, + profileGivenName: convo.profileName ?? null, + profileFamilyName: convo.profileFamilyName ?? null, + systemFamilyName: convo.systemFamilyName ?? null, + systemGivenName: convo.systemGivenName ?? null, + systemNickname: convo.systemNickname ?? null, + hideStory: convo.hideStory === true, + identityKey: identityKey?.publicKey || null, + avatarColor: toAvatarColor(convo.color) ?? null, + keyTransparencyData: options?.keyTransparencyData ?? null, - // Integer values match so we can use it as is - identityState: identityKey?.verified ?? 0, - nickname: - nicknameGivenName || nicknameFamilyName - ? { - given: nicknameGivenName, - family: nicknameFamilyName, - } - : null, - note, + // Integer values match so we can use it as is + identityState: identityKey?.verified ?? 0, + nickname: + nicknameGivenName || nicknameFamilyName + ? { + given: nicknameGivenName ?? null, + family: nicknameFamilyName ?? null, + } + : null, + note: note ?? null, + }, + }, }; - } else if (isGroupV2(convo) && convo.masterKey) { + } + if (isGroupV2(convo) && convo.masterKey) { let storySendMode: Backups.Group.StorySendMode; switch (convo.storySendMode) { case StorySendMode.Always: @@ -1304,85 +1327,105 @@ export class BackupExportStream extends Readable { const masterKey = Bytes.fromBase64(convo.masterKey); - res.group = { - masterKey, - whitelisted: convo.profileSharing, - hideStory: convo.hideStory === true, - storySendMode, - blocked: convo.groupId - ? itemStorage.blocked.isGroupBlocked(convo.groupId) - : false, - avatarColor: toAvatarColor(convo.color), - snapshot: { - title: { - title: convo.name?.trim() ?? '', - }, - description: - convo.description != null - ? { - descriptionText: convo.description.trim(), - } - : null, - avatarUrl: convo.avatar?.url, - disappearingMessagesTimer: - convo.expireTimer != null - ? { - disappearingMessagesDuration: DurationInSeconds.toSeconds( - convo.expireTimer - ), - } - : null, - accessControl: convo.accessControl, - version: convo.revision || 0, - members: convo.membersV2?.map(member => { - return { - joinedAtVersion: member.joinedAtVersion, - ...(member.labelString - ? { - labelEmoji: member.labelEmoji, - labelString: member.labelString, - } - : undefined), - role: member.role, - userId: this.#aciToBytes(member.aci), - }; - }), - membersPendingProfileKey: convo.pendingMembersV2?.map(member => { - return { - member: { - userId: this.#serviceIdToBytes(member.serviceId), - role: member.role, - joinedAtVersion: 0, + return { + id: recipientId, + destination: { + group: { + masterKey, + whitelisted: convo.profileSharing ?? null, + hideStory: convo.hideStory === true, + storySendMode, + blocked: convo.groupId + ? itemStorage.blocked.isGroupBlocked(convo.groupId) + : false, + avatarColor: toAvatarColor(convo.color) ?? null, + snapshot: { + title: { + content: { + title: convo.name?.trim() ?? '', + }, }, - addedByUserId: this.#aciToBytes(member.addedByUserId), - timestamp: getSafeLongFromTimestamp(member.timestamp), - }; - }), - membersPendingAdminApproval: convo.pendingAdminApprovalV2?.map( - member => { - return { - userId: this.#aciToBytes(member.aci), - timestamp: getSafeLongFromTimestamp(member.timestamp), - }; - } - ), - membersBanned: convo.bannedMembersV2?.map(member => { - return { - userId: this.#serviceIdToBytes(member.serviceId), - timestamp: getSafeLongFromTimestamp(member.timestamp), - }; - }), - inviteLinkPassword: convo.groupInviteLinkPassword - ? Bytes.fromBase64(convo.groupInviteLinkPassword) - : null, - announcementsOnly: convo.announcementsOnly === true, + description: + convo.description != null + ? { + content: { + descriptionText: convo.description.trim(), + }, + } + : null, + avatarUrl: convo.avatar?.url ?? null, + disappearingMessagesTimer: + convo.expireTimer != null + ? { + content: { + disappearingMessagesDuration: + DurationInSeconds.toSeconds(convo.expireTimer), + }, + } + : null, + accessControl: convo.accessControl + ? { + ...convo.accessControl, + memberLabel: convo.accessControl.memberLabel ?? null, + } + : null, + version: convo.revision || 0, + members: + convo.membersV2?.map(member => { + return { + joinedAtVersion: member.joinedAtVersion, + ...(member.labelString + ? { + labelEmoji: member.labelEmoji ?? null, + labelString: member.labelString, + } + : { + labelEmoji: null, + labelString: null, + }), + role: member.role, + userId: this.#aciToBytes(member.aci), + }; + }) ?? null, + membersPendingProfileKey: + convo.pendingMembersV2?.map(member => { + return { + member: { + userId: this.#serviceIdToBytes(member.serviceId), + role: member.role, + joinedAtVersion: 0, + labelEmoji: null, + labelString: null, + }, + addedByUserId: this.#aciToBytes(member.addedByUserId), + timestamp: getSafeLongFromTimestamp(member.timestamp), + }; + }) ?? null, + membersPendingAdminApproval: + convo.pendingAdminApprovalV2?.map(member => { + return { + userId: this.#aciToBytes(member.aci), + timestamp: getSafeLongFromTimestamp(member.timestamp), + }; + }) ?? null, + membersBanned: + convo.bannedMembersV2?.map(member => { + return { + userId: this.#serviceIdToBytes(member.serviceId), + timestamp: getSafeLongFromTimestamp(member.timestamp), + }; + }) ?? null, + inviteLinkPassword: convo.groupInviteLinkPassword + ? Bytes.fromBase64(convo.groupInviteLinkPassword) + : null, + announcementsOnly: convo.announcementsOnly === true, + }, + }, }, }; - } else { - return undefined; } - return res; + return null; } async #toChatItem( @@ -1392,7 +1435,7 @@ export class BackupExportStream extends Readable { callHistoryByCallId, pinnedMessagesByMessageId, }: ToChatItemOptionsType - ): Promise { + ): Promise { const conversation = window.ConversationController.get( message.conversationId ); @@ -1440,7 +1483,7 @@ export class BackupExportStream extends Readable { } } - let authorId: Long | undefined; + let authorId: bigint | undefined; const isOutgoing = message.type === 'outgoing'; const isIncoming = message.type === 'incoming'; @@ -1467,7 +1510,7 @@ export class BackupExportStream extends Readable { // Fix conversation id for misattributed e164-only incoming 1:1 // messages. - if (authorId.neq(convoAuthor)) { + if (authorId !== convoAuthor) { authorId = convoAuthor; this.#stats.fixedDirectMessages += 1; } @@ -1485,13 +1528,11 @@ export class BackupExportStream extends Readable { strictAssert(authorId, 'Incoming/outgoing messages require an author'); } - let expireStartDate: Long | undefined; - let expiresInMs: Long | undefined; + let expireStartDate: bigint | null = null; + let expiresInMs: bigint | null = null; if (message.expireTimer != null) { - expiresInMs = Long.fromNumber( - DurationInSeconds.toMillis(message.expireTimer) - ); + expiresInMs = BigInt(DurationInSeconds.toMillis(message.expireTimer)); if (message.expirationStartTimestamp != null) { expireStartDate = getSafeLongFromTimestamp( @@ -1500,7 +1541,10 @@ export class BackupExportStream extends Readable { } } - const result: Backups.IChatItem = { + const base: Omit< + Backups.ChatItem.Params, + 'directionalDetails' | 'item' | 'pinDetails' | 'revisions' + > = { chatId, authorId, dateSent: getSafeLongFromTimestamp( @@ -1508,10 +1552,14 @@ export class BackupExportStream extends Readable { ), expireStartDate, expiresInMs, - revisions: [], sms: message.sms === true, }; + let directionalDetails: Backups.ChatItem.Params['directionalDetails'] = + null; + let item: Backups.ChatItem.Params['item']; + let revisions: Backups.ChatItem.Params['revisions'] = null; + if (!isNormalBubble(message)) { const { patch, kind } = await this.#toChatItemFromNonBubble({ authorId, @@ -1534,69 +1582,96 @@ export class BackupExportStream extends Readable { }); if (authorId === me) { - result.outgoing = this.#getOutgoingMessageDetails( - message.sent_at, - message, - { conversationId: message.conversationId } - ); + directionalDetails = { + outgoing: this.#getOutgoingMessageDetails( + message.sent_at, + message, + { conversationId: message.conversationId } + ), + }; } else { - result.incoming = this.#getIncomingMessageDetails(message); + directionalDetails = { + incoming: this.#getIncomingMessageDetails(message), + }; } } else if (kind === NonBubbleResultKind.Directionless) { - result.directionless = {}; + directionalDetails = { + directionless: {}, + }; } else { throw missingCaseError(kind); } - return { ...result, ...patch }; + return { + ...base, + directionalDetails, + ...patch, + revisions: null, + pinDetails: null, + }; } const { contact, sticker } = message; if (isTapToView(message)) { - result.viewOnceMessage = await this.#toViewOnceMessage({ - message, - }); + item = { + viewOnceMessage: await this.#toViewOnceMessage({ + message, + }), + }; } else if (message.deletedForEveryone) { if (message.deletedForEveryoneByAdminAci) { - result.adminDeletedMessage = { - adminId: this.#getOrPushPrivateRecipient({ - serviceId: message.deletedForEveryoneByAdminAci, - }), + item = { + adminDeletedMessage: { + adminId: this.#getOrPushPrivateRecipient({ + serviceId: message.deletedForEveryoneByAdminAci, + }), + }, }; } else { - result.remoteDeletedMessage = {}; + item = { remoteDeletedMessage: {} }; } } else if (messageHasPaymentEvent(message)) { const { payment } = message; switch (payment.kind) { case PaymentEventKind.ActivationRequest: { - result.directionless = {}; - result.updateMessage = { - simpleUpdate: { - type: Backups.SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST, + directionalDetails = { directionless: {} }; + item = { + updateMessage: { + update: { + simpleUpdate: { + type: Backups.SimpleChatUpdate.Type + .PAYMENT_ACTIVATION_REQUEST, + }, + }, }, }; break; } case PaymentEventKind.Activation: { - result.directionless = {}; - result.updateMessage = { - simpleUpdate: { - type: Backups.SimpleChatUpdate.Type.PAYMENTS_ACTIVATED, + directionalDetails = { directionless: {} }; + item = { + updateMessage: { + update: { + simpleUpdate: { + type: Backups.SimpleChatUpdate.Type.PAYMENTS_ACTIVATED, + }, + }, }, }; break; } case PaymentEventKind.Notification: - result.paymentNotification = { - note: payment.note ?? null, - amountMob: payment.amountMob, - feeMob: payment.feeMob, - transactionDetails: payment.transactionDetailsBase64 - ? Backups.PaymentNotification.TransactionDetails.decode( - Bytes.fromBase64(payment.transactionDetailsBase64) - ) - : undefined, + item = { + paymentNotification: { + note: payment.note ?? null, + amountMob: payment.amountMob ?? null, + feeMob: payment.feeMob ?? null, + transactionDetails: payment.transactionDetailsBase64 + ? Backups.PaymentNotification.TransactionDetails.decode( + Bytes.fromBase64(payment.transactionDetailsBase64) + ) + : null, + }, }; break; default: @@ -1604,60 +1679,83 @@ export class BackupExportStream extends Readable { } } else if (contact && contact[0]) { const [contactDetails] = contact; - const contactMessage = new Backups.ContactMessage(); - contactMessage.contact = { - ...contactDetails, - number: contactDetails.number?.map(number => ({ - ...number, - type: numberToPhoneType(number.type), - })), - email: contactDetails.email?.map(email => ({ - ...email, - type: numberToPhoneType(email.type), - })), - address: contactDetails.address?.map(address => ({ - ...address, - type: numberToAddressType(address.type), - })), - avatar: contactDetails.avatar?.avatar - ? await this.#processAttachment({ - attachment: contactDetails.avatar.avatar, - messageReceivedAt: message.received_at, - }) - : undefined, + item = { + contactMessage: { + contact: { + name: contactDetails.name + ? { + givenName: contactDetails.name.givenName ?? null, + familyName: contactDetails.name.familyName ?? null, + prefix: contactDetails.name.prefix ?? null, + suffix: contactDetails.name.suffix ?? null, + middleName: contactDetails.name.middleName ?? null, + nickname: contactDetails.name.nickname ?? null, + } + : null, + number: + contactDetails.number?.map(number => ({ + value: number.value, + type: numberToPhoneType(number.type), + label: number.label ?? null, + })) ?? null, + email: + contactDetails.email?.map(email => ({ + value: email.value, + type: numberToPhoneType(email.type), + label: email.label ?? null, + })) ?? null, + address: + contactDetails.address?.map(address => ({ + type: numberToAddressType(address.type), + label: address.label ?? null, + street: address.street ?? null, + pobox: address.pobox ?? null, + neighborhood: address.neighborhood ?? null, + city: address.city ?? null, + region: address.region ?? null, + postcode: address.postcode ?? null, + country: address.country ?? null, + })) ?? null, + avatar: contactDetails.avatar?.avatar + ? await this.#processAttachment({ + attachment: contactDetails.avatar.avatar, + messageReceivedAt: message.received_at, + }) + : null, + organization: contactDetails.organization ?? null, + }, + reactions: this.#getMessageReactions(message), + }, }; - - const reactions = this.#getMessageReactions(message); - if (reactions != null) { - contactMessage.reactions = reactions; - } - - result.contactMessage = contactMessage; } else if (sticker) { - const stickerProto = new Backups.Sticker(); - stickerProto.emoji = sticker.emoji; - stickerProto.packId = Bytes.fromHex(sticker.packId); - stickerProto.packKey = Bytes.fromBase64(sticker.packKey); - stickerProto.stickerId = sticker.stickerId; - stickerProto.data = sticker.data - ? await this.#processAttachment({ - attachment: sticker.data, - messageReceivedAt: message.received_at, - }) - : undefined; - - result.stickerMessage = { - sticker: stickerProto, - reactions: this.#getMessageReactions(message), + item = { + stickerMessage: { + sticker: { + emoji: sticker.emoji ?? null, + packId: Bytes.fromHex(sticker.packId), + packKey: Bytes.fromBase64(sticker.packKey), + stickerId: sticker.stickerId, + data: sticker.data + ? await this.#processAttachment({ + attachment: sticker.data, + messageReceivedAt: message.received_at, + }) + : null, + }, + reactions: this.#getMessageReactions(message), + }, }; } else if (isGiftBadge(message)) { const { giftBadge } = message; strictAssert(giftBadge != null, 'Message must have gift badge'); if (giftBadge.state === GiftBadgeStates.Failed) { - result.giftBadge = { - state: Backups.GiftBadge.State.FAILED, + item = { + giftBadge: { + state: Backups.GiftBadge.State.FAILED, + receiptCredentialPresentation: null, + }, }; } else { let state: Backups.GiftBadge.State; @@ -1675,134 +1773,151 @@ export class BackupExportStream extends Readable { throw missingCaseError(giftBadge); } - result.giftBadge = { - receiptCredentialPresentation: Bytes.fromBase64( - giftBadge.receiptCredentialPresentation - ), - state, + item = { + giftBadge: { + receiptCredentialPresentation: Bytes.fromBase64( + giftBadge.receiptCredentialPresentation + ), + state, + }, }; } } else if (message.storyReplyContext || message.storyReaction) { - result.directStoryReplyMessage = await this.#toDirectStoryReplyMessage({ - message, - }); + item = { + directStoryReplyMessage: await this.#toDirectStoryReplyMessage({ + message, + }), + }; - result.revisions = await this.#toChatItemRevisions(result, message); + revisions = await this.#toChatItemRevisions(base, item, message); } else if (message.poll) { const { poll } = message; - const pollMessage = new Backups.Poll(); + item = { + poll: { + question: poll.question, + allowMultiple: poll.allowMultiple, + hasEnded: poll.terminatedAt != null, + options: poll.options.map((optionText, optionIndex) => { + const votesForThisOption = new Map(); - pollMessage.question = poll.question; - pollMessage.allowMultiple = poll.allowMultiple; - pollMessage.hasEnded = poll.terminatedAt != null; + if (poll.votes) { + for (const vote of poll.votes) { + // Skip votes that have not been sent + if (vote.sendStateByConversationId) { + continue; + } - pollMessage.options = poll.options.map((optionText, optionIndex) => { - const pollOption = new Backups.Poll.PollOption(); - pollOption.option = optionText; - - const votesForThisOption = new Map(); - - if (poll.votes) { - for (const vote of poll.votes) { - // Skip votes that have not been sent - if (vote.sendStateByConversationId) { - continue; + // If we somehow have multiple votes from the same person + // (shouldn't happen, just in case) only keep the highest voteCount + const maybeExistingVoteFromThisConversation = + votesForThisOption.get(vote.fromConversationId); + if ( + vote.optionIndexes.includes(optionIndex) && + (!maybeExistingVoteFromThisConversation || + vote.voteCount > maybeExistingVoteFromThisConversation) + ) { + votesForThisOption.set( + vote.fromConversationId, + vote.voteCount + ); + } + } } - // If we somehow have multiple votes from the same person - // (shouldn't happen, just in case) only keep the highest voteCount - const maybeExistingVoteFromThisConversation = - votesForThisOption.get(vote.fromConversationId); - if ( - vote.optionIndexes.includes(optionIndex) && - (!maybeExistingVoteFromThisConversation || - vote.voteCount > maybeExistingVoteFromThisConversation) - ) { - votesForThisOption.set(vote.fromConversationId, vote.voteCount); - } - } - } + const votes = Array.from(votesForThisOption.entries()).map( + ([conversationId, voteCount]) => { + const voterConvo = + window.ConversationController.get(conversationId); + let voterId: bigint | null = null; + if (voterConvo) { + voterId = this.#getOrPushPrivateRecipient( + voterConvo.attributes + ); + } - pollOption.votes = Array.from(votesForThisOption.entries()).map( - ([conversationId, voteCount]) => { - const pollVote = new Backups.Poll.PollOption.PollVote(); + return { + voterId, + voteCount, + }; + } + ); - const voterConvo = - window.ConversationController.get(conversationId); - if (voterConvo) { - pollVote.voterId = this.#getOrPushPrivateRecipient( - voterConvo.attributes - ); - } - - pollVote.voteCount = voteCount; - return pollVote; - } - ); - - return pollOption; - }); - - const reactions = this.#getMessageReactions(message); - if (reactions != null) { - pollMessage.reactions = reactions; - } - - result.poll = pollMessage; + return { + option: optionText, + votes, + }; + }), + reactions: this.#getMessageReactions(message), + }, + }; } else if (message.pollTerminateNotification) { // TODO (DESKTOP-9282) return undefined; } else if (message.isErased) { return undefined; } else { - result.standardMessage = await this.#toStandardMessage({ - message, - }); + item = { + standardMessage: await this.#toStandardMessage({ + message, + }), + }; - result.revisions = await this.#toChatItemRevisions(result, message); + revisions = await this.#toChatItemRevisions(base, item, message); } - if (isOutgoing) { - result.outgoing = this.#getOutgoingMessageDetails( - message.sent_at, - message, - { conversationId: message.conversationId } - ); - } else { - result.incoming = this.#getIncomingMessageDetails(message); + if (directionalDetails == null) { + if (isOutgoing) { + directionalDetails = { + outgoing: this.#getOutgoingMessageDetails(message.sent_at, message, { + conversationId: message.conversationId, + }), + }; + } else { + directionalDetails = { + incoming: this.#getIncomingMessageDetails(message), + }; + } } const pinnedMessage = pinnedMessagesByMessageId[message.id]; + + let pinDetails: Backups.ChatItem.PinDetails.Params | null; if (pinnedMessage != null) { - const pinnedAtTimestamp = Long.fromNumber(pinnedMessage.pinnedAt); + const pinnedAtTimestamp = BigInt(pinnedMessage.pinnedAt); - let pinExpiresAtTimestamp: Long | null = null; - let pinNeverExpires: true | null = null; + let pinExpiry: Backups.ChatItem.PinDetails.Params['pinExpiry']; if (pinnedMessage.expiresAt != null) { - pinExpiresAtTimestamp = Long.fromNumber(pinnedMessage.expiresAt); + pinExpiry = { + pinExpiresAtTimestamp: BigInt(pinnedMessage.expiresAt), + }; } else { - pinNeverExpires = true; + pinExpiry = { + pinNeverExpires: true, + }; } - - result.pinDetails = { - pinnedAtTimestamp, - pinExpiresAtTimestamp, - pinNeverExpires, - }; + pinDetails = { pinnedAtTimestamp, pinExpiry }; + } else { + pinDetails = null; } - return result; + return { + ...base, + directionalDetails, + item, + revisions, + pinDetails, + }; } #aciToBytes(aci: AciString | string): Uint8Array { return Aci.parseFromServiceIdString(aci).getRawUuidBytes(); } - #aciToBytesOrUndefined(aci: AciString | string): Uint8Array | undefined { + #aciToBytesOrNull(aci: AciString | string): Uint8Array | null { if (isAciString(aci)) { return Aci.parseFromServiceIdString(aci).getRawUuidBytes(); } - return undefined; + return null; } #serviceIdToBytes(serviceId: ServiceIdString): Uint8Array { @@ -1821,10 +1936,14 @@ export class BackupExportStream extends Readable { const { authorId, callHistoryByCallId, message } = options; const logId = `toChatItemUpdate(${getMessageIdForLogging(message)})`; - const updateMessage = new Backups.ChatUpdateMessage(); + const updateMessage: Backups.ChatUpdateMessage.Params = { + update: null, + }; - const patch: Backups.IChatItem = { - updateMessage, + const patch: NonBubbleResultType['patch'] = { + item: { + updateMessage, + }, }; if (isCallHistory(message)) { @@ -1851,8 +1970,6 @@ export class BackupExportStream extends Readable { } if (isGroup(conversation.attributes)) { - const groupCall = new Backups.GroupCall(); - strictAssert( callHistory.mode === CallMode.Group, 'in group, should be group call' @@ -1863,6 +1980,7 @@ export class BackupExportStream extends Readable { } const { ringerId, startedById } = callHistory; + let ringerRecipientId: bigint | null = null; if (ringerId) { const ringerConversation = window.ConversationController.get(ringerId); @@ -1872,12 +1990,12 @@ export class BackupExportStream extends Readable { ); } - const recipientId = this.#getRecipientId( + ringerRecipientId = this.#getRecipientId( ringerConversation.attributes ); - groupCall.ringerRecipientId = recipientId; } + let startedCallRecipientId: bigint | null = null; if (startedById) { const startedByConvo = window.ConversationController.get(startedById); if (!startedByConvo) { @@ -1886,53 +2004,70 @@ export class BackupExportStream extends Readable { ); } - const recipientId = this.#getRecipientId(startedByConvo.attributes); - groupCall.startedCallRecipientId = recipientId; + startedCallRecipientId = this.#getRecipientId( + startedByConvo.attributes + ); } + let groupCallId: bigint; try { - groupCall.callId = Long.fromString(callId); + groupCallId = BigInt(callId); } catch (e) { - // Could not convert callId to long; likely a legacy backfilled callId with uuid + // Could not convert callId to bigint + // likely a legacy backfilled callId with uuid // TODO (DESKTOP-8007) - groupCall.callId = Long.fromNumber(0); + groupCallId = 0n; } - groupCall.state = toGroupCallStateProto(callHistory.status); - groupCall.startedCallTimestamp = Long.fromNumber(callHistory.timestamp); + const state = toGroupCallStateProto(callHistory.status); + const startedCallTimestamp = BigInt(callHistory.timestamp); + let endedCallTimestamp: bigint | null = null; if (callHistory.endedTimestamp != null) { - groupCall.endedCallTimestamp = getSafeLongFromTimestamp( + endedCallTimestamp = getSafeLongFromTimestamp( callHistory.endedTimestamp ); } - groupCall.read = message.seenStatus === SeenStatus.Seen; + const read = message.seenStatus === SeenStatus.Seen; - updateMessage.groupCall = groupCall; + updateMessage.update = { + groupCall: { + startedCallRecipientId, + ringerRecipientId, + callId: groupCallId, + state, + startedCallTimestamp, + endedCallTimestamp, + read, + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } - const individualCall = new Backups.IndividualCall(); const { direction, type, status, timestamp } = callHistory; if (status === GroupCallStatus.Deleted) { return { kind: NonBubbleResultKind.Drop }; } + let individualCallId: bigint; try { - individualCall.callId = Long.fromString(callId); + individualCallId = BigInt(callId); } catch (e) { // TODO (DESKTOP-8007) - // Could not convert callId to long; likely a legacy backfilled callId with uuid - individualCall.callId = Long.fromNumber(0); + // Could not convert callId to bigint; likely a legacy backfilled callId with uuid + individualCallId = 0n; } - individualCall.type = toIndividualCallTypeProto(type); - individualCall.direction = toIndividualCallDirectionProto(direction); - individualCall.state = toIndividualCallStateProto(status, direction); - individualCall.startedCallTimestamp = Long.fromNumber(timestamp); - individualCall.read = message.seenStatus === SeenStatus.Seen; - - updateMessage.individualCall = individualCall; + updateMessage.update = { + individualCall: { + callId: individualCallId, + type: toIndividualCallTypeProto(type), + direction: toIndividualCallDirectionProto(direction), + state: toIndividualCallStateProto(status, direction), + startedCallTimestamp: BigInt(timestamp), + read: message.seenStatus === SeenStatus.Seen, + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } @@ -1954,22 +2089,26 @@ export class BackupExportStream extends Readable { patch.authorId = this.#getRecipientId({ serviceId: options.aboutMe.aci, }); - const groupChatUpdate = new Backups.GroupChangeChatUpdate(); - - const timerUpdate = new Backups.GroupExpirationTimerUpdate(); - timerUpdate.expiresInMs = Long.fromNumber(expiresInMs); + let updaterAci: Uint8Array | null = null; if (sourceServiceId && Aci.parseFromServiceIdString(sourceServiceId)) { - timerUpdate.updaterAci = uuidToBytes(sourceServiceId); + updaterAci = uuidToBytes(sourceServiceId); } - const innerUpdate = new Backups.GroupChangeChatUpdate.Update(); - - innerUpdate.groupExpirationTimerUpdate = timerUpdate; - - groupChatUpdate.updates = [innerUpdate]; - - updateMessage.groupChange = groupChatUpdate; + updateMessage.update = { + groupChange: { + updates: [ + { + update: { + groupExpirationTimerUpdate: { + expiresInMs: BigInt(expiresInMs), + updaterAci, + }, + }, + }, + ], + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } @@ -1987,25 +2126,25 @@ export class BackupExportStream extends Readable { } } - const expirationTimerChange = new Backups.ExpirationTimerChatUpdate(); - expirationTimerChange.expiresInMs = Long.fromNumber(expiresInMs); - - updateMessage.expirationTimerChange = expirationTimerChange; + updateMessage.update = { + expirationTimerChange: { + expiresInMs: BigInt(expiresInMs), + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } if (isGroupV2Change(message)) { - updateMessage.groupChange = await this.toGroupV2Update(message, options); + updateMessage.update = { + groupChange: await this.toGroupV2Update(message, options), + }; strictAssert(this.#ourConversation?.id, 'our conversation must exist'); patch.authorId = this.#getOrPushPrivateRecipient(this.#ourConversation); return { kind: NonBubbleResultKind.Directionless, patch }; } if (isKeyChange(message)) { - const simpleUpdate = new Backups.SimpleChatUpdate(); - simpleUpdate.type = Backups.SimpleChatUpdate.Type.IDENTITY_UPDATE; - const conversation = window.ConversationController.get( message.conversationId ); @@ -2026,14 +2165,18 @@ export class BackupExportStream extends Readable { patch.authorId = this.#getOrPushPrivateRecipient(target.attributes); } - updateMessage.simpleUpdate = simpleUpdate; + updateMessage.update = { + simpleUpdate: { + type: Backups.SimpleChatUpdate.Type.IDENTITY_UPDATE, + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } if (isPinnedMessageNotification(message)) { - let targetAuthorId: Long | null = null; - let targetSentTimestamp: Long | null = null; + let targetAuthorId: bigint | null = null; + let targetSentTimestamp: bigint | null = null; if (message.pinMessage == null) { log.warn( @@ -2043,23 +2186,22 @@ export class BackupExportStream extends Readable { return { kind: NonBubbleResultKind.Drop }; } - targetSentTimestamp = Long.fromNumber( - message.pinMessage.targetSentTimestamp - ); + targetSentTimestamp = BigInt(message.pinMessage.targetSentTimestamp); targetAuthorId = this.#getOrPushPrivateRecipient({ serviceId: message.pinMessage.targetAuthorAci, }); - updateMessage.pinMessage = new Backups.PinMessageUpdate({ - targetSentTimestamp, - authorId: targetAuthorId, - }); + updateMessage.update = { + pinMessage: { + targetSentTimestamp, + authorId: targetAuthorId, + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } if (isProfileChange(message)) { - const profileChange = new Backups.ProfileChangeChatUpdate(); if (!message.profileChange?.newName || !message.profileChange?.oldName) { return { kind: NonBubbleResultKind.Drop }; } @@ -2079,10 +2221,13 @@ export class BackupExportStream extends Readable { } const { newName, oldName } = message.profileChange; - profileChange.newName = newName; - profileChange.previousName = oldName; - updateMessage.profileChange = profileChange; + updateMessage.update = { + profileChange: { + newName, + previousName: oldName, + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } @@ -2094,12 +2239,13 @@ export class BackupExportStream extends Readable { ); } - const simpleUpdate = new Backups.SimpleChatUpdate(); - simpleUpdate.type = message.verified - ? Backups.SimpleChatUpdate.Type.IDENTITY_VERIFIED - : Backups.SimpleChatUpdate.Type.IDENTITY_DEFAULT; - - updateMessage.simpleUpdate = simpleUpdate; + updateMessage.update = { + simpleUpdate: { + type: message.verified + ? Backups.SimpleChatUpdate.Type.IDENTITY_VERIFIED + : Backups.SimpleChatUpdate.Type.IDENTITY_DEFAULT, + }, + }; if (message.verifiedChanged) { // This will override authorId on the original chatItem @@ -2112,16 +2258,20 @@ export class BackupExportStream extends Readable { } if (isChangeNumberNotification(message)) { - updateMessage.simpleUpdate = { - type: Backups.SimpleChatUpdate.Type.CHANGE_NUMBER, + updateMessage.update = { + simpleUpdate: { + type: Backups.SimpleChatUpdate.Type.CHANGE_NUMBER, + }, }; return { kind: NonBubbleResultKind.Directionless, patch }; } if (isJoinedSignalNotification(message)) { - updateMessage.simpleUpdate = { - type: Backups.SimpleChatUpdate.Type.JOINED_SIGNAL, + updateMessage.update = { + simpleUpdate: { + type: Backups.SimpleChatUpdate.Type.JOINED_SIGNAL, + }, }; return { kind: NonBubbleResultKind.Directionless, patch }; @@ -2134,15 +2284,21 @@ export class BackupExportStream extends Readable { ); const { renderInfo } = message.titleTransition; if (renderInfo.e164) { - updateMessage.learnedProfileChange = { - e164: Long.fromString(renderInfo.e164), + updateMessage.update = { + learnedProfileChange: { + previousName: { e164: BigInt(renderInfo.e164) }, + }, }; } else { strictAssert( renderInfo.username, 'Title transition must have username or e164' ); - updateMessage.learnedProfileChange = { username: renderInfo.username }; + updateMessage.update = { + learnedProfileChange: { + previousName: { username: renderInfo.username }, + }, + }; } return { kind: NonBubbleResultKind.Directionless, patch }; @@ -2173,26 +2329,26 @@ export class BackupExportStream extends Readable { throw missingCaseError(event); } - updateMessage.simpleUpdate = { type }; + updateMessage.update = { simpleUpdate: { type } }; return { kind: NonBubbleResultKind.Directionless, patch }; } if (isDeliveryIssue(message)) { - updateMessage.simpleUpdate = { - type: Backups.SimpleChatUpdate.Type.BAD_DECRYPT, + updateMessage.update = { + simpleUpdate: { + type: Backups.SimpleChatUpdate.Type.BAD_DECRYPT, + }, }; return { kind: NonBubbleResultKind.Directionless, patch }; } if (isConversationMerge(message)) { - const threadMerge = new Backups.ThreadMergeChatUpdate(); const e164 = message.conversationMerge?.renderInfo.e164; if (!e164) { return { kind: NonBubbleResultKind.Drop }; } - threadMerge.previousE164 = Long.fromString(e164); // Conversation merges generated on Desktop side never has // `sourceServiceId` and thus are attributed to our conversation. @@ -2202,7 +2358,11 @@ export class BackupExportStream extends Readable { id: message.conversationId, }); - updateMessage.threadMerge = threadMerge; + updateMessage.update = { + threadMerge: { + previousE164: BigInt(e164), + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } @@ -2213,8 +2373,10 @@ export class BackupExportStream extends Readable { return { kind: NonBubbleResultKind.Drop }; } - updateMessage.sessionSwitchover = { - e164: Long.fromString(e164), + updateMessage.update = { + sessionSwitchover: { + e164: BigInt(e164), + }, }; return { kind: NonBubbleResultKind.Directionless, patch }; } @@ -2231,27 +2393,31 @@ export class BackupExportStream extends Readable { // Create a GV2 tombstone for a deprecated GV1 notification if (isGroupUpdate(message)) { - updateMessage.groupChange = { - updates: [ - { - genericGroupUpdate: { - updaterAci: message.sourceServiceId - ? this.#aciToBytesOrUndefined(message.sourceServiceId) - : undefined, + updateMessage.update = { + groupChange: { + updates: [ + { + update: { + genericGroupUpdate: { + updaterAci: message.sourceServiceId + ? this.#aciToBytesOrNull(message.sourceServiceId) + : null, + }, + }, }, - }, - ], + ], + }, }; return { kind: NonBubbleResultKind.Directionless, patch }; } if (isUnsupportedMessage(message)) { - const simpleUpdate = new Backups.SimpleChatUpdate(); - simpleUpdate.type = - Backups.SimpleChatUpdate.Type.UNSUPPORTED_PROTOCOL_MESSAGE; - - updateMessage.simpleUpdate = simpleUpdate; + updateMessage.update = { + simpleUpdate: { + type: Backups.SimpleChatUpdate.Type.UNSUPPORTED_PROTOCOL_MESSAGE, + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } @@ -2259,9 +2425,7 @@ export class BackupExportStream extends Readable { if (isGroupV1Migration(message)) { const { groupMigration } = message; - const groupChatUpdate = new Backups.GroupChangeChatUpdate(); - - groupChatUpdate.updates = []; + const updates: Array = []; const areWeInvited = groupMigration?.areWeInvited ?? false; const droppedMemberCount = @@ -2277,56 +2441,69 @@ export class BackupExportStream extends Readable { let addedItem = false; if (areWeInvited) { - const container = new Backups.GroupChangeChatUpdate.Update(); - container.groupV2MigrationSelfInvitedUpdate = - new Backups.GroupV2MigrationSelfInvitedUpdate(); - groupChatUpdate.updates.push(container); + updates.push({ + update: { + groupV2MigrationSelfInvitedUpdate: {}, + }, + }); addedItem = true; } if (invitedMemberCount > 0) { - const container = new Backups.GroupChangeChatUpdate.Update(); - const update = new Backups.GroupV2MigrationInvitedMembersUpdate(); - update.invitedMembersCount = invitedMemberCount; - container.groupV2MigrationInvitedMembersUpdate = update; - groupChatUpdate.updates.push(container); + updates.push({ + update: { + groupV2MigrationInvitedMembersUpdate: { + invitedMembersCount: invitedMemberCount, + }, + }, + }); addedItem = true; } if (droppedMemberCount > 0) { - const container = new Backups.GroupChangeChatUpdate.Update(); - const update = new Backups.GroupV2MigrationDroppedMembersUpdate(); - update.droppedMembersCount = droppedMemberCount; - container.groupV2MigrationDroppedMembersUpdate = update; - groupChatUpdate.updates.push(container); + updates.push({ + update: { + groupV2MigrationDroppedMembersUpdate: { + droppedMembersCount: droppedMemberCount, + }, + }, + }); addedItem = true; } if (!addedItem) { - const container = new Backups.GroupChangeChatUpdate.Update(); - container.groupV2MigrationUpdate = new Backups.GroupV2MigrationUpdate(); - groupChatUpdate.updates.push(container); + updates.push({ + update: { + groupV2MigrationUpdate: {}, + }, + }); } - updateMessage.groupChange = groupChatUpdate; + updateMessage.update = { + groupChange: { + updates, + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } if (isEndSession(message)) { - const simpleUpdate = new Backups.SimpleChatUpdate(); - simpleUpdate.type = Backups.SimpleChatUpdate.Type.END_SESSION; - - updateMessage.simpleUpdate = simpleUpdate; + updateMessage.update = { + simpleUpdate: { + type: Backups.SimpleChatUpdate.Type.END_SESSION, + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } if (isChatSessionRefreshed(message)) { - const simpleUpdate = new Backups.SimpleChatUpdate(); - simpleUpdate.type = Backups.SimpleChatUpdate.Type.CHAT_SESSION_REFRESH; - - updateMessage.simpleUpdate = simpleUpdate; + updateMessage.update = { + simpleUpdate: { + type: Backups.SimpleChatUpdate.Type.CHAT_SESSION_REFRESH, + }, + }; return { kind: NonBubbleResultKind.Directionless, patch }; } @@ -2341,7 +2518,7 @@ export class BackupExportStream extends Readable { options: { aboutMe: AboutMe; } - ): Promise { + ): Promise { const logId = `toGroupV2Update(${getMessageIdForLogging(message)})`; const { groupV2Change } = message; @@ -2351,132 +2528,133 @@ export class BackupExportStream extends Readable { } const { from, details } = groupV2Change; - const updates: Array = []; + const updates: Array = []; details.forEach(detail => { - const update = new Backups.GroupChangeChatUpdate.Update(); const { type } = detail; if (type === 'create') { - const innerUpdate = new Backups.GroupCreationUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - update.groupCreationUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupCreationUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + }, + }, + }); } else if (type === 'access-attributes') { - const innerUpdate = - new Backups.GroupAttributesAccessLevelChangeUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.accessLevel = detail.newPrivilege; - - update.groupAttributesAccessLevelChangeUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupAttributesAccessLevelChangeUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + accessLevel: detail.newPrivilege, + }, + }, + }); } else if (type === 'access-members') { - const innerUpdate = - new Backups.GroupMembershipAccessLevelChangeUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.accessLevel = detail.newPrivilege; - - update.groupMembershipAccessLevelChangeUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupMembershipAccessLevelChangeUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + accessLevel: detail.newPrivilege, + }, + }, + }); } else if (type === 'access-invite-link') { - const innerUpdate = new Backups.GroupInviteLinkAdminApprovalUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.linkRequiresAdminApproval = - detail.newPrivilege === - SignalService.AccessControl.AccessRequired.ADMINISTRATOR; - - update.groupInviteLinkAdminApprovalUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupInviteLinkAdminApprovalUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + linkRequiresAdminApproval: + detail.newPrivilege === + SignalService.AccessControl.AccessRequired.ADMINISTRATOR, + }, + }, + }); } else if (type === 'access-member-label') { - const innerUpdate = - new Backups.GroupMemberLabelAccessLevelChangeUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.accessLevel = detail.newPrivilege; - - update.groupMemberLabelAccessLevelChangeUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupMemberLabelAccessLevelChangeUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + accessLevel: detail.newPrivilege, + }, + }, + }); } else if (type === 'announcements-only') { - const innerUpdate = new Backups.GroupAnnouncementOnlyChangeUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.isAnnouncementOnly = detail.announcementsOnly; - - update.groupAnnouncementOnlyChangeUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupAnnouncementOnlyChangeUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + isAnnouncementOnly: detail.announcementsOnly, + }, + }, + }); } else if (type === 'avatar') { - const innerUpdate = new Backups.GroupAvatarUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.wasRemoved = detail.removed; - - update.groupAvatarUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupAvatarUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + wasRemoved: detail.removed, + }, + }, + }); } else if (type === 'title') { - const innerUpdate = new Backups.GroupNameUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.newGroupName = detail.newTitle; - - update.groupNameUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupNameUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + newGroupName: detail.newTitle ?? null, + }, + }, + }); } else if (type === 'group-link-add') { - const innerUpdate = new Backups.GroupInviteLinkEnabledUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.linkRequiresAdminApproval = - detail.privilege === - SignalService.AccessControl.AccessRequired.ADMINISTRATOR; - - update.groupInviteLinkEnabledUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupInviteLinkEnabledUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + linkRequiresAdminApproval: + detail.privilege === + SignalService.AccessControl.AccessRequired.ADMINISTRATOR, + }, + }, + }); } else if (type === 'group-link-reset') { - const innerUpdate = new Backups.GroupInviteLinkResetUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - - update.groupInviteLinkResetUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupInviteLinkResetUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + }, + }, + }); } else if (type === 'group-link-remove') { - const innerUpdate = new Backups.GroupInviteLinkDisabledUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - - update.groupInviteLinkDisabledUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupInviteLinkDisabledUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + }, + }, + }); } else if (type === 'member-add') { if (from && from === detail.aci) { - const innerUpdate = new Backups.GroupMemberJoinedUpdate(); - innerUpdate.newMemberAci = this.#aciToBytes(from); - - update.groupMemberJoinedUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupMemberJoinedUpdate: { + newMemberAci: this.#aciToBytes(from), + }, + }, + }); return; } - const innerUpdate = new Backups.GroupMemberAddedUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.newMemberAci = this.#aciToBytes(detail.aci); - - update.groupMemberAddedUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupMemberAddedUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + newMemberAci: this.#aciToBytes(detail.aci), + hadOpenInvitation: null, + inviterAci: null, + }, + }, + }); } else if (type === 'member-add-from-invite') { const { aci, pni } = detail; if ( @@ -2485,87 +2663,92 @@ export class BackupExportStream extends Readable { (aci && from === aci) || checkServiceIdEquivalence(from, aci)) ) { - const innerUpdate = new Backups.GroupInvitationAcceptedUpdate(); - innerUpdate.newMemberAci = this.#aciToBytes(detail.aci); - if (detail.inviter) { - innerUpdate.inviterAci = this.#aciToBytes(detail.inviter); - } - update.groupInvitationAcceptedUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupInvitationAcceptedUpdate: { + newMemberAci: this.#aciToBytes(detail.aci), + inviterAci: detail.inviter + ? this.#aciToBytes(detail.inviter) + : null, + }, + }, + }); return; } - const innerUpdate = new Backups.GroupMemberAddedUpdate(); - innerUpdate.newMemberAci = this.#aciToBytes(detail.aci); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - if (detail.inviter) { - innerUpdate.inviterAci = this.#aciToBytes(detail.inviter); - } - innerUpdate.hadOpenInvitation = true; - - update.groupMemberAddedUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupMemberAddedUpdate: { + newMemberAci: this.#aciToBytes(detail.aci), + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + inviterAci: detail.inviter + ? this.#aciToBytes(detail.inviter) + : null, + hadOpenInvitation: true, + }, + }, + }); } else if (type === 'member-add-from-link') { - const innerUpdate = new Backups.GroupMemberJoinedByLinkUpdate(); - innerUpdate.newMemberAci = this.#aciToBytes(detail.aci); - - update.groupMemberJoinedByLinkUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupMemberJoinedByLinkUpdate: { + newMemberAci: this.#aciToBytes(detail.aci), + }, + }, + }); } else if (type === 'member-add-from-admin-approval') { - const innerUpdate = new Backups.GroupJoinRequestApprovalUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - - innerUpdate.requestorAci = this.#aciToBytes(detail.aci); - innerUpdate.wasApproved = true; - - update.groupJoinRequestApprovalUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupJoinRequestApprovalUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + requestorAci: this.#aciToBytes(detail.aci), + wasApproved: true, + }, + }, + }); } else if (type === 'member-privilege') { - const innerUpdate = new Backups.GroupAdminStatusUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - - innerUpdate.memberAci = this.#aciToBytes(detail.aci); - innerUpdate.wasAdminStatusGranted = - detail.newPrivilege === SignalService.Member.Role.ADMINISTRATOR; - - update.groupAdminStatusUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupAdminStatusUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + memberAci: this.#aciToBytes(detail.aci), + wasAdminStatusGranted: + detail.newPrivilege === SignalService.Member.Role.ADMINISTRATOR, + }, + }, + }); } else if (type === 'member-remove') { if (from && from === detail.aci) { - const innerUpdate = new Backups.GroupMemberLeftUpdate(); - innerUpdate.aci = this.#aciToBytes(from); - - update.groupMemberLeftUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupMemberLeftUpdate: { + aci: this.#aciToBytes(from), + }, + }, + }); return; } - const innerUpdate = new Backups.GroupMemberRemovedUpdate(); - if (from) { - innerUpdate.removerAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.removedAci = this.#aciToBytes(detail.aci); - - update.groupMemberRemovedUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupMemberRemovedUpdate: { + removerAci: from ? this.#aciToBytesOrNull(from) : null, + removedAci: this.#aciToBytes(detail.aci), + }, + }, + }); } else if (type === 'pending-add-one') { if ( (aboutMe.aci && detail.serviceId === aboutMe.aci) || (aboutMe.pni && detail.serviceId === aboutMe.pni) ) { - const innerUpdate = new Backups.SelfInvitedToGroupUpdate(); - if (from) { - innerUpdate.inviterAci = this.#aciToBytesOrUndefined(from); - } - - update.selfInvitedToGroupUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + selfInvitedToGroupUpdate: { + inviterAci: from ? this.#aciToBytesOrNull(from) : null, + }, + }, + }); return; } if ( @@ -2573,163 +2756,172 @@ export class BackupExportStream extends Readable { ((aboutMe.aci && from === aboutMe.aci) || (aboutMe.pni && from === aboutMe.pni)) ) { - const innerUpdate = new Backups.SelfInvitedOtherUserToGroupUpdate(); - innerUpdate.inviteeServiceId = this.#serviceIdToBytes( - detail.serviceId - ); - - update.selfInvitedOtherUserToGroupUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + selfInvitedOtherUserToGroupUpdate: { + inviteeServiceId: this.#serviceIdToBytes(detail.serviceId), + }, + }, + }); return; } - const innerUpdate = new Backups.GroupUnknownInviteeUpdate(); - if (from) { - innerUpdate.inviterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.inviteeCount = 1; - - update.groupUnknownInviteeUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupUnknownInviteeUpdate: { + inviterAci: from ? this.#aciToBytesOrNull(from) : null, + inviteeCount: 1, + }, + }, + }); } else if (type === 'pending-add-many') { - const innerUpdate = new Backups.GroupUnknownInviteeUpdate(); - if (from) { - innerUpdate.inviterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.inviteeCount = detail.count; - - update.groupUnknownInviteeUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupUnknownInviteeUpdate: { + inviterAci: from ? this.#aciToBytesOrNull(from) : null, + inviteeCount: detail.count, + }, + }, + }); } else if (type === 'pending-remove-one') { if ((from && from === detail.serviceId) || detail.serviceId == null) { - const innerUpdate = new Backups.GroupInvitationDeclinedUpdate(); - if (detail.inviter) { - innerUpdate.inviterAci = this.#aciToBytes(detail.inviter); - } - if (isAciString(detail.serviceId)) { - innerUpdate.inviteeAci = this.#aciToBytes(detail.serviceId); - } - - update.groupInvitationDeclinedUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupInvitationDeclinedUpdate: { + inviterAci: detail.inviter + ? this.#aciToBytes(detail.inviter) + : null, + inviteeAci: isAciString(detail.serviceId) + ? this.#aciToBytes(detail.serviceId) + : null, + }, + }, + }); return; } if ( (aboutMe.aci && detail.serviceId === aboutMe.aci) || (aboutMe.pni && detail.serviceId === aboutMe.pni) ) { - const innerUpdate = new Backups.GroupSelfInvitationRevokedUpdate(); - if (from) { - innerUpdate.revokerAci = this.#aciToBytesOrUndefined(from); - } - - update.groupSelfInvitationRevokedUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupSelfInvitationRevokedUpdate: { + revokerAci: from ? this.#aciToBytesOrNull(from) : null, + }, + }, + }); return; } - const innerUpdate = new Backups.GroupInvitationRevokedUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - innerUpdate.invitees = [ - { - inviterAci: isAciString(detail.inviter) - ? this.#aciToBytes(detail.inviter) - : undefined, - inviteeAci: isAciString(detail.serviceId) - ? this.#aciToBytes(detail.serviceId) - : undefined, - inviteePni: isPniString(detail.serviceId) - ? this.#serviceIdToBytes(detail.serviceId) - : undefined, + updates.push({ + update: { + groupInvitationRevokedUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + invitees: [ + { + inviterAci: isAciString(detail.inviter) + ? this.#aciToBytes(detail.inviter) + : null, + inviteeAci: isAciString(detail.serviceId) + ? this.#aciToBytes(detail.serviceId) + : null, + inviteePni: isPniString(detail.serviceId) + ? this.#serviceIdToBytes(detail.serviceId) + : null, + }, + ], + }, }, - ]; - - update.groupInvitationRevokedUpdate = innerUpdate; - updates.push(update); + }); } else if (type === 'pending-remove-many') { - const innerUpdate = new Backups.GroupInvitationRevokedUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - - innerUpdate.invitees = []; - for (let i = 0, max = detail.count; i < max; i += 1) { - // Yes, we're adding totally empty invitees. This is okay. - innerUpdate.invitees.push({}); - } - - update.groupInvitationRevokedUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupInvitationRevokedUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + invitees: Array.from({ length: detail.count }, () => { + return { + // Yes, we're adding totally empty invitees. This is okay. + inviterAci: null, + inviteeAci: null, + inviteePni: null, + }; + }), + }, + }, + }); } else if (type === 'admin-approval-add-one') { - const innerUpdate = new Backups.GroupJoinRequestUpdate(); - innerUpdate.requestorAci = this.#aciToBytes(detail.aci); - - update.groupJoinRequestUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupJoinRequestUpdate: { + requestorAci: this.#aciToBytes(detail.aci), + }, + }, + }); } else if (type === 'admin-approval-remove-one') { if (from && detail.aci && from === detail.aci) { - const innerUpdate = new Backups.GroupJoinRequestCanceledUpdate(); - innerUpdate.requestorAci = this.#aciToBytes(detail.aci); - - update.groupJoinRequestCanceledUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupJoinRequestCanceledUpdate: { + requestorAci: this.#aciToBytes(detail.aci), + }, + }, + }); return; } - const innerUpdate = new Backups.GroupJoinRequestApprovalUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - - innerUpdate.requestorAci = this.#aciToBytes(detail.aci); - innerUpdate.wasApproved = false; - - update.groupJoinRequestApprovalUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupJoinRequestApprovalUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + requestorAci: this.#aciToBytes(detail.aci), + wasApproved: false, + }, + }, + }); } else if (type === 'admin-approval-bounce') { // We can't express all we need in GroupSequenceOfRequestsAndCancelsUpdate, so we // add an additional groupJoinRequestUpdate to express that there // is an approval pending. if (detail.isApprovalPending) { - const innerUpdate = new Backups.GroupJoinRequestUpdate(); - innerUpdate.requestorAci = this.#aciToBytes(detail.aci); - // We need to create another update since the items we put in Update are oneof - const secondUpdate = new Backups.GroupChangeChatUpdate.Update(); - secondUpdate.groupJoinRequestUpdate = innerUpdate; - updates.push(secondUpdate); + updates.push({ + update: { + groupJoinRequestUpdate: { + requestorAci: this.#aciToBytes(detail.aci), + }, + }, + }); // not returning because we really do want both of these } - const innerUpdate = - new Backups.GroupSequenceOfRequestsAndCancelsUpdate(); - innerUpdate.requestorAci = this.#aciToBytes(detail.aci); - innerUpdate.count = detail.times; - - update.groupSequenceOfRequestsAndCancelsUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupSequenceOfRequestsAndCancelsUpdate: { + requestorAci: this.#aciToBytes(detail.aci), + count: detail.times, + }, + }, + }); } else if (type === 'description') { - const innerUpdate = new Backups.GroupDescriptionUpdate(); - innerUpdate.newDescription = detail.removed - ? undefined - : detail.description; - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - - update.groupDescriptionUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + groupDescriptionUpdate: { + newDescription: detail.removed + ? null + : (detail.description ?? null), + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + }, + }, + }); } else if (type === 'summary') { - const innerUpdate = new Backups.GenericGroupUpdate(); - if (from) { - innerUpdate.updaterAci = this.#aciToBytesOrUndefined(from); - } - - update.genericGroupUpdate = innerUpdate; - updates.push(update); + updates.push({ + update: { + genericGroupUpdate: { + updaterAci: from ? this.#aciToBytesOrNull(from) : null, + }, + }, + }); } else { throw missingCaseError(type); } @@ -2739,23 +2931,20 @@ export class BackupExportStream extends Readable { throw new Error(`${logId}: No updates generated from message`); } - const groupUpdate = new Backups.GroupChangeChatUpdate(); - groupUpdate.updates = updates; - - return groupUpdate; + return { updates }; } async #toQuote({ message, }: { message: Pick; - }): Promise { + }): Promise { const { quote } = message; if (!quote) { return null; } - let authorId: Long; + let authorId: bigint; if (quote.authorAci) { authorId = this.#getOrPushPrivateRecipient({ serviceId: quote.authorAci, @@ -2790,7 +2979,7 @@ export class BackupExportStream extends Readable { targetSentTimestamp: quote.referencedMessageNotFound || quote.id == null ? null - : Long.fromNumber(quote.id), + : BigInt(quote.id), authorId, text: quote.text != null @@ -2803,16 +2992,16 @@ export class BackupExportStream extends Readable { quote.attachments.map( async ( attachment: QuotedAttachmentType - ): Promise => { + ): Promise => { return { contentType: attachment.contentType, - fileName: attachment.fileName, + fileName: attachment.fileName ?? null, thumbnail: attachment.thumbnail ? await this.#processMessageAttachment({ attachment: attachment.thumbnail, message, }) - : undefined, + : null, }; } ) @@ -2821,7 +3010,7 @@ export class BackupExportStream extends Readable { }; } - #toBodyRange(range: RawBodyRange): Backups.IBodyRange | null { + #toBodyRange(range: RawBodyRange): Backups.BodyRange.Params | null { const { data: parsedRange, error } = safeParseStrict( bodyRangeSchema, range @@ -2831,33 +3020,32 @@ export class BackupExportStream extends Readable { log.warn('toBodyRange: Dropping invalid body range', toLogFormat(error)); return null; } - return { start: parsedRange.start, length: parsedRange.length, - - ...('mentionAci' in parsedRange - ? { - mentionAci: this.#aciToBytes(parsedRange.mentionAci), - } - : { - // Numeric values are compatible between backup and message protos - style: parsedRange.style, - }), + associatedValue: + 'mentionAci' in parsedRange + ? { + mentionAci: this.#aciToBytes(parsedRange.mentionAci), + } + : { + // Numeric values are compatible between backup and message protos + style: parsedRange.style, + }, }; } #toBodyRanges( ranges: ReadonlyArray | undefined - ): Array | undefined { + ): Array | null { if (!ranges?.length) { - return undefined; + return null; } const result = ranges .map(range => this.#toBodyRange(range)) .filter(isNotNil); - return result.length > 0 ? result : undefined; + return result.length > 0 ? result : null; } #getMessageAttachmentFlag( @@ -2894,19 +3082,19 @@ export class BackupExportStream extends Readable { }: { attachment: AttachmentType; message: Pick; - }): Promise { + }): Promise { const { clientUuid } = attachment; const filePointer = await this.#processAttachment({ attachment, messageReceivedAt: message.received_at, }); - return new Backups.MessageAttachment({ + return { pointer: filePointer, flag: this.#getMessageAttachmentFlag(message, attachment), wasDownloaded: isDownloaded(attachment), - clientUuid: clientUuid ? uuidToBytes(clientUuid) : undefined, - }); + clientUuid: clientUuid ? uuidToBytes(clientUuid) : null, + }; } async #processAttachment({ @@ -2915,7 +3103,7 @@ export class BackupExportStream extends Readable { }: { attachment: AttachmentType; messageReceivedAt: number; - }): Promise { + }): Promise { const { filePointer, backupJob } = await getFilePointerForAttachment({ attachment, backupOptions: this.options, @@ -2960,21 +3148,22 @@ export class BackupExportStream extends Readable { #getMessageReactions({ reactions, - }: Pick): - | Array - | undefined { + }: Pick< + MessageAttributesType, + 'reactions' + >): Array | null { if (reactions == null) { - return undefined; + return null; } - return reactions?.map((reaction, sortOrder) => { + return reactions.map((reaction, sortOrder): Backups.Reaction.Params => { return { - emoji: reaction.emoji, + emoji: reaction.emoji ?? null, authorId: this.#getOrPushPrivateRecipient({ id: reaction.fromId, }), sentTimestamp: getSafeLongFromTimestamp(reaction.timestamp), - sortOrder: Long.fromNumber(sortOrder), + sortOrder: BigInt(sortOrder), }; }); } @@ -2992,7 +3181,7 @@ export class BackupExportStream extends Readable { | 'serverTimestamp' | 'readStatus' | 'unidentifiedDeliveryReceived' - >): Backups.ChatItem.IIncomingMessageDetails { + >): Backups.ChatItem.IncomingMessageDetails.Params { const dateReceived = editMessageReceivedAtMs || receivedAtMs; return { dateReceived: @@ -3023,7 +3212,7 @@ export class BackupExportStream extends Readable { | 'editMessageReceivedAtMs' >, { conversationId }: { conversationId: string } - ): Backups.ChatItem.IOutgoingMessageDetails { + ): Backups.ChatItem.OutgoingMessageDetails.Params { const sealedSenderServiceIds = new Set(unidentifiedDeliveries); const errorMap = new Map( errors?.map(({ serviceId, name }) => { @@ -3031,7 +3220,7 @@ export class BackupExportStream extends Readable { }) ); - const sendStatuses = new Array(); + const sendStatuses = new Array(); for (const [id, entry] of Object.entries(sendStateByConversationId)) { const target = window.ConversationController.get(id); if (!target) { @@ -3056,68 +3245,62 @@ export class BackupExportStream extends Readable { ? getSafeLongFromTimestamp(entry.updatedAt) : null; - const sendStatus = new Backups.SendStatus({ recipientId, timestamp }); - const sealedSender = serviceId ? sealedSenderServiceIds.has(serviceId) : false; + let deliveryStatus: Backups.SendStatus.Params['deliveryStatus']; switch (entry.status) { case SendStatus.Pending: - sendStatus.pending = new Backups.SendStatus.Pending(); + deliveryStatus = { pending: {} }; break; case SendStatus.Sent: - sendStatus.sent = new Backups.SendStatus.Sent({ - sealedSender, - }); + deliveryStatus = { sent: { sealedSender } }; break; case SendStatus.Delivered: - sendStatus.delivered = new Backups.SendStatus.Delivered({ - sealedSender, - }); + deliveryStatus = { delivered: { sealedSender } }; break; case SendStatus.Read: - sendStatus.read = new Backups.SendStatus.Read({ - sealedSender, - }); + deliveryStatus = { read: { sealedSender } }; break; case SendStatus.Viewed: - sendStatus.viewed = new Backups.SendStatus.Viewed({ - sealedSender, - }); + deliveryStatus = { viewed: { sealedSender } }; break; case SendStatus.Skipped: - sendStatus.skipped = {}; + deliveryStatus = { skipped: {} }; break; case SendStatus.Failed: { - sendStatus.failed = new Backups.SendStatus.Failed(); + let reason: Backups.SendStatus.Failed.Params['reason']; + if (!serviceId) { - break; - } - const errorName = errorMap.get(serviceId); - if (!errorName) { - break; + reason = null; + } else { + const errorName = errorMap.get(serviceId); + if (!errorName) { + reason = null; + } else if (errorName === 'OutgoingIdentityKeyError') { + reason = + Backups.SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH; + } else if (errorName === 'UnknownError') { + // See ts/backups/import.ts + reason = Backups.SendStatus.Failed.FailureReason.UNKNOWN; + } else { + reason = Backups.SendStatus.Failed.FailureReason.NETWORK; + } } - const identityKeyMismatch = errorName === 'OutgoingIdentityKeyError'; - if (identityKeyMismatch) { - sendStatus.failed.reason = - Backups.SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH; - } else if (errorName === 'UnknownError') { - // See ts/backups/import.ts - sendStatus.failed.reason = - Backups.SendStatus.Failed.FailureReason.UNKNOWN; - } else { - sendStatus.failed.reason = - Backups.SendStatus.Failed.FailureReason.NETWORK; - } + deliveryStatus = { failed: { reason } }; break; } default: throw missingCaseError(entry.status); } - sendStatuses.push(sendStatus); + sendStatuses.push({ + recipientId, + timestamp, + deliveryStatus, + }); } const dateReceived = editMessageReceivedAtMs || receivedAtMs; @@ -3143,7 +3326,7 @@ export class BackupExportStream extends Readable { | 'received_at' | 'timestamp' >; - }): Promise { + }): Promise { if ( message.body && isBodyTooLong(message.body, MAX_BACKUP_MESSAGE_BODY_BYTE_LENGTH) @@ -3164,26 +3347,28 @@ export class BackupExportStream extends Readable { }); }) ) - : undefined, + : null, ...(await this.#toTextAndLongTextFields(message)), linkPreview: message.preview ? await Promise.all( - message.preview.map(async preview => { - return { - url: preview.url, - title: preview.title, - description: preview.description, - date: getSafeLongFromTimestamp(preview.date), - image: preview.image - ? await this.#processAttachment({ - attachment: preview.image, - messageReceivedAt: message.received_at, - }) - : undefined, - }; - }) + message.preview.map( + async (preview): Promise => { + return { + url: preview.url, + title: preview.title ?? null, + description: preview.description ?? null, + date: getSafeLongFromTimestamp(preview.date), + image: preview.image + ? await this.#processAttachment({ + attachment: preview.image, + messageReceivedAt: message.received_at, + }) + : null, + }; + } + ) ) - : undefined, + : null, reactions: this.#getMessageReactions(message), }; } @@ -3200,17 +3385,17 @@ export class BackupExportStream extends Readable { | 'received_at' | 'reactions' >; - }): Promise { - const result = new Backups.DirectStoryReplyMessage({ - reactions: this.#getMessageReactions(message), - }); + }): Promise { + const reactions = this.#getMessageReactions(message); + let reply: Backups.DirectStoryReplyMessage.Params['reply']; if (message.storyReaction) { - result.emoji = message.storyReaction.emoji; + reply = { emoji: message.storyReaction.emoji }; } else { - result.textReply = await this.#toTextAndLongTextFields(message); + reply = { textReply: await this.#toTextAndLongTextFields(message) }; } - return result; + + return { reply, reactions }; } async #toTextAndLongTextFields( @@ -3218,10 +3403,7 @@ export class BackupExportStream extends Readable { MessageAttributesType, 'bodyAttachment' | 'body' | 'bodyRanges' | 'received_at' > - ): Promise<{ - longText: Backups.IFilePointer | undefined; - text: Backups.IText | undefined; - }> { + ): Promise { const includeLongTextAttachment = message.bodyAttachment && !isDownloaded(message.bodyAttachment); const bodyRanges = this.#toBodyRanges(message.bodyRanges); @@ -3236,7 +3418,7 @@ export class BackupExportStream extends Readable { attachment: message.bodyAttachment, messageReceivedAt: message.received_at, }) - : undefined, + : null, text: includeText ? { body: message.body @@ -3246,10 +3428,10 @@ export class BackupExportStream extends Readable { ? MAX_MESSAGE_BODY_BYTE_LENGTH : MAX_BACKUP_MESSAGE_BODY_BYTE_LENGTH ) - : undefined, + : null, bodyRanges, } - : undefined, + : null, }; } @@ -3260,7 +3442,7 @@ export class BackupExportStream extends Readable { MessageAttributesType, 'attachments' | 'received_at' | 'reactions' >; - }): Promise { + }): Promise { const attachment = message.attachments?.at(0); // Integration tests use the 'link-and-sync' version of export, which will include // view-once attachments @@ -3279,12 +3461,16 @@ export class BackupExportStream extends Readable { } async #toChatItemRevisions( - parent: Backups.IChatItem, + parent: Pick< + Backups.ChatItem.Params, + 'chatId' | 'authorId' | 'expireStartDate' | 'expiresInMs' | 'sms' + >, + parentItem: NonNullable, message: MessageAttributesType - ): Promise | undefined> { + ): Promise | null> { const { editHistory } = message; if (editHistory == null) { - return undefined; + return null; } const isOutgoing = message.type === 'outgoing'; @@ -3294,7 +3480,7 @@ export class BackupExportStream extends Readable { // The first history is the copy of the current message .slice(1) .map(async history => { - const result: Backups.IChatItem = { + const base: Omit = { // Required fields chatId: parent.chatId, authorId: parent.authorId, @@ -3303,35 +3489,44 @@ export class BackupExportStream extends Readable { expiresInMs: parent.expiresInMs, sms: parent.sms, - // Directional details - outgoing: isOutgoing - ? this.#getOutgoingMessageDetails(history.timestamp, history, { - conversationId: message.conversationId, - }) - : undefined, - incoming: isOutgoing - ? undefined - : this.#getIncomingMessageDetails(history), + directionalDetails: isOutgoing + ? { + outgoing: this.#getOutgoingMessageDetails( + history.timestamp, + history, + { conversationId: message.conversationId } + ), + } + : { + incoming: this.#getIncomingMessageDetails(history), + }, + + revisions: null, + pinDetails: null, }; - if (parent.directStoryReplyMessage) { - result.directStoryReplyMessage = - await this.#toDirectStoryReplyMessage({ + let item: Backups.ChatItem.Params['item']; + if (parentItem.directStoryReplyMessage) { + item = { + directStoryReplyMessage: await this.#toDirectStoryReplyMessage({ message: history, - }); + }), + }; } else { - result.standardMessage = await this.#toStandardMessage({ - message: history, - }); + item = { + standardMessage: await this.#toStandardMessage({ + message: history, + }), + }; } - return result; + return { ...base, item }; }) // Backups use oldest to newest order .reverse() ); } - #toCustomChatColors(): Array { + #toCustomChatColors(): Array { const customColors = itemStorage.get('customColors'); if (!customColors) { return []; @@ -3358,9 +3553,9 @@ export class BackupExportStream extends Readable { map.set(uuid, color); } - const result = new Array(); + const result = new Array(); for (const [uuid, color] of map.entries()) { - const id = Long.fromNumber(result.length + 1); + const id = BigInt(result.length + 1); this.#customColorIdByUuid.set(uuid, id); const start = desktopHslToRgbInt( @@ -3372,7 +3567,7 @@ export class BackupExportStream extends Readable { if (color.end == null) { result.push({ id, - solid: start, + color: { solid: start }, }); } else { const end = desktopHslToRgbInt( @@ -3388,10 +3583,12 @@ export class BackupExportStream extends Readable { result.push({ id, - gradient: { - colors: [start, end], - positions: [0, 1], - angle: (backupAngle + 360) % 360, + color: { + gradient: { + colors: [start, end], + positions: [0, 1], + angle: (backupAngle + 360) % 360, + }, }, }); } @@ -3400,7 +3597,7 @@ export class BackupExportStream extends Readable { return result; } - #toDefaultChatStyle(): Backups.IChatStyle | null { + #toDefaultChatStyle(): Backups.ChatStyle.Params | null { const defaultColor = itemStorage.get('defaultConversationColor'); const wallpaperPhotoPointer = itemStorage.get( 'defaultWallpaperPhotoPointer' @@ -3429,11 +3626,7 @@ export class BackupExportStream extends Readable { customColorId, dimWallpaperInDarkMode, autoBubbleColor, - }: LocalChatStyle): Backups.IChatStyle | null { - const result: Backups.IChatStyle = { - dimWallpaperInDarkMode, - }; - + }: LocalChatStyle): Backups.ChatStyle.Params | null { // The defaults if ( (color == null || color === 'ultramarine') && @@ -3445,18 +3638,21 @@ export class BackupExportStream extends Readable { return null; } + let wallpaper: Backups.ChatStyle.Params['wallpaper']; if (Bytes.isNotEmpty(wallpaperPhotoPointer)) { - result.wallpaperPhoto = Backups.FilePointer.decode(wallpaperPhotoPointer); + wallpaper = { + wallpaperPhoto: Backups.FilePointer.decode(wallpaperPhotoPointer), + }; } else if (wallpaperPreset) { - result.wallpaperPreset = wallpaperPreset; + wallpaper = { wallpaperPreset }; + } else { + wallpaper = null; } + let bubbleColor: Backups.ChatStyle.Params['bubbleColor']; if (color == null || autoBubbleColor) { - result.autoBubbleColor = {}; - return result; - } - - if (color === 'custom') { + bubbleColor = { autoBubbleColor: {} }; + } else if (color === 'custom') { strictAssert( customColorId != null, 'No custom color id for custom color' @@ -3465,84 +3661,90 @@ export class BackupExportStream extends Readable { const index = this.#customColorIdByUuid.get(customColorId); strictAssert(index != null, 'Missing custom color'); - result.customColorId = index; - return result; + bubbleColor = { customColorId: index }; + } else { + const { BubbleColorPreset } = Backups.ChatStyle; + + let preset: Backups.ChatStyle.BubbleColorPreset; + switch (color) { + case 'ultramarine': + preset = BubbleColorPreset.SOLID_ULTRAMARINE; + break; + case 'crimson': + preset = BubbleColorPreset.SOLID_CRIMSON; + break; + case 'vermilion': + preset = BubbleColorPreset.SOLID_VERMILION; + break; + case 'burlap': + preset = BubbleColorPreset.SOLID_BURLAP; + break; + case 'forest': + preset = BubbleColorPreset.SOLID_FOREST; + break; + case 'wintergreen': + preset = BubbleColorPreset.SOLID_WINTERGREEN; + break; + case 'teal': + preset = BubbleColorPreset.SOLID_TEAL; + break; + case 'blue': + preset = BubbleColorPreset.SOLID_BLUE; + break; + case 'indigo': + preset = BubbleColorPreset.SOLID_INDIGO; + break; + case 'violet': + preset = BubbleColorPreset.SOLID_VIOLET; + break; + case 'plum': + preset = BubbleColorPreset.SOLID_PLUM; + break; + case 'taupe': + preset = BubbleColorPreset.SOLID_TAUPE; + break; + case 'steel': + preset = BubbleColorPreset.SOLID_STEEL; + break; + case 'ember': + preset = BubbleColorPreset.GRADIENT_EMBER; + break; + case 'midnight': + preset = BubbleColorPreset.GRADIENT_MIDNIGHT; + break; + case 'infrared': + preset = BubbleColorPreset.GRADIENT_INFRARED; + break; + case 'lagoon': + preset = BubbleColorPreset.GRADIENT_LAGOON; + break; + case 'fluorescent': + preset = BubbleColorPreset.GRADIENT_FLUORESCENT; + break; + case 'basil': + preset = BubbleColorPreset.GRADIENT_BASIL; + break; + case 'sublime': + preset = BubbleColorPreset.GRADIENT_SUBLIME; + break; + case 'sea': + preset = BubbleColorPreset.GRADIENT_SEA; + break; + case 'tangerine': + preset = BubbleColorPreset.GRADIENT_TANGERINE; + break; + default: + throw missingCaseError(color); + } + + bubbleColor = { bubbleColorPreset: preset }; } - const { BubbleColorPreset } = Backups.ChatStyle; - - switch (color) { - case 'ultramarine': - result.bubbleColorPreset = BubbleColorPreset.SOLID_ULTRAMARINE; - break; - case 'crimson': - result.bubbleColorPreset = BubbleColorPreset.SOLID_CRIMSON; - break; - case 'vermilion': - result.bubbleColorPreset = BubbleColorPreset.SOLID_VERMILION; - break; - case 'burlap': - result.bubbleColorPreset = BubbleColorPreset.SOLID_BURLAP; - break; - case 'forest': - result.bubbleColorPreset = BubbleColorPreset.SOLID_FOREST; - break; - case 'wintergreen': - result.bubbleColorPreset = BubbleColorPreset.SOLID_WINTERGREEN; - break; - case 'teal': - result.bubbleColorPreset = BubbleColorPreset.SOLID_TEAL; - break; - case 'blue': - result.bubbleColorPreset = BubbleColorPreset.SOLID_BLUE; - break; - case 'indigo': - result.bubbleColorPreset = BubbleColorPreset.SOLID_INDIGO; - break; - case 'violet': - result.bubbleColorPreset = BubbleColorPreset.SOLID_VIOLET; - break; - case 'plum': - result.bubbleColorPreset = BubbleColorPreset.SOLID_PLUM; - break; - case 'taupe': - result.bubbleColorPreset = BubbleColorPreset.SOLID_TAUPE; - break; - case 'steel': - result.bubbleColorPreset = BubbleColorPreset.SOLID_STEEL; - break; - case 'ember': - result.bubbleColorPreset = BubbleColorPreset.GRADIENT_EMBER; - break; - case 'midnight': - result.bubbleColorPreset = BubbleColorPreset.GRADIENT_MIDNIGHT; - break; - case 'infrared': - result.bubbleColorPreset = BubbleColorPreset.GRADIENT_INFRARED; - break; - case 'lagoon': - result.bubbleColorPreset = BubbleColorPreset.GRADIENT_LAGOON; - break; - case 'fluorescent': - result.bubbleColorPreset = BubbleColorPreset.GRADIENT_FLUORESCENT; - break; - case 'basil': - result.bubbleColorPreset = BubbleColorPreset.GRADIENT_BASIL; - break; - case 'sublime': - result.bubbleColorPreset = BubbleColorPreset.GRADIENT_SUBLIME; - break; - case 'sea': - result.bubbleColorPreset = BubbleColorPreset.GRADIENT_SEA; - break; - case 'tangerine': - result.bubbleColorPreset = BubbleColorPreset.GRADIENT_TANGERINE; - break; - default: - throw missingCaseError(color); - } - - return result; + return { + dimWallpaperInDarkMode: dimWallpaperInDarkMode ?? null, + wallpaper, + bubbleColor, + }; } } diff --git a/ts/services/backups/import.preload.ts b/ts/services/backups/import.preload.ts index 1c2d3374c0..d0b5e531ab 100644 --- a/ts/services/backups/import.preload.ts +++ b/ts/services/backups/import.preload.ts @@ -11,7 +11,6 @@ import pMap from 'p-map'; import { Writable } from 'node:stream'; import lodash from 'lodash'; import { CallLinkRootKey } from '@signalapp/ringrtc'; -import type Long from 'long'; import { Backups, SignalService } from '../../protobuf/index.std.js'; import { DataReader, DataWriter } from '../../sql/Client.preload.js'; @@ -167,6 +166,8 @@ import { expiresTooSoonForBackup } from './util/expiration.std.js'; import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js'; import type { PinnedMessageParams } from '../../types/PinnedMessage.std.js'; import type { ThemeType } from '../../util/preload.preload.js'; +import { toNumber } from '../../util/toNumber.std.js'; +import { isKnownProtoEnumMember } from '../../util/isKnownProtoEnumMember.std.js'; const { isNumber } = lodash; @@ -184,9 +185,12 @@ type ChatItemParseResult = { const SKIP = 'SKIP' as const; function phoneToContactFormType( - type: Backups.ContactAttachment.Phone.Type | null | undefined + type?: Backups.ContactAttachment.Phone['type'] ): ContactFormType { const { Type } = Backups.ContactAttachment.Phone; + if (!isKnownProtoEnumMember(Type, type)) { + return ContactFormType.HOME; + } switch (type) { case Type.HOME: return ContactFormType.HOME; @@ -206,9 +210,12 @@ function phoneToContactFormType( } function emailToContactFormType( - type: Backups.ContactAttachment.Email.Type | null | undefined + type?: Backups.ContactAttachment.Email['type'] ): ContactFormType { const { Type } = Backups.ContactAttachment.Email; + if (!isKnownProtoEnumMember(Type, type)) { + return ContactFormType.HOME; + } switch (type) { case Type.HOME: return ContactFormType.HOME; @@ -228,9 +235,12 @@ function emailToContactFormType( } function addressToContactAddressType( - type: Backups.ContactAttachment.PostalAddress.Type | null | undefined + type?: Backups.ContactAttachment.PostalAddress['type'] ): ContactAddressType { const { Type } = Backups.ContactAttachment.PostalAddress; + if (!isKnownProtoEnumMember(Type, type)) { + return ContactAddressType.HOME; + } switch (type) { case Type.HOME: return ContactAddressType.HOME; @@ -253,12 +263,12 @@ export class BackupImportStream extends Writable { #logId = 'BackupImportStream(unknown)'; #aboutMe: AboutMe | undefined; - readonly #recipientIdToConvo = new Map(); + readonly #recipientIdToConvo = new Map(); - readonly #recipientIdToCallLink = new Map(); + readonly #recipientIdToCallLink = new Map(); readonly #adminCallLinksToHasCall = new Map(); - readonly #chatIdToConvo = new Map(); + readonly #chatIdToConvo = new Map(); readonly #conversations = new Map(); @@ -274,8 +284,8 @@ export class BackupImportStream extends Writable { #ourConversation?: ConversationAttributesType; #pinnedConversations = new Array<[number, string]>(); #customColorById = new Map(); - #releaseNotesRecipientId: Long | undefined; - #releaseNotesChatId: Long | undefined; + #releaseNotesRecipientId: bigint | undefined; + #releaseNotesChatId: bigint | undefined; #pinnedMessages: Array = []; #frameErrorCount: number = 0; #backupTier: BackupLevel | undefined; @@ -308,7 +318,7 @@ export class BackupImportStream extends Writable { log.info(`${this.#logId}: got BackupInfo`); - if (info.version?.toNumber() !== BACKUP_VERSION) { + if (info.version !== BACKUP_VERSION) { throw new UnsupportedBackupVersion(info.version); } @@ -516,9 +526,11 @@ export class BackupImportStream extends Writable { options: { aboutMe?: AboutMe } ): Promise { const { aboutMe } = options; + const { item } = frame; + strictAssert(item, `${this.#logId}: Missing Frame.item`); - if (frame.account) { - await this.#fromAccount(frame.account); + if (item.account) { + await this.#fromAccount(item.account); // We run this outside of try catch below because failure to restore // the account data is fatal. @@ -526,15 +538,17 @@ export class BackupImportStream extends Writable { } try { - if (frame.recipient) { - const { recipient } = frame; + if (item.recipient) { + const { recipient } = item; strictAssert(recipient.id != null, 'Recipient must have an id'); - const recipientId = recipient.id.toNumber(); + + const { destination } = recipient; + strictAssert(destination, 'Missing Recipient.destination'); let convo: ConversationAttributesType; - if (recipient.contact) { - convo = await this.#fromContact(recipient.contact); - } else if (recipient.releaseNotes) { + if (destination.contact) { + convo = await this.#fromContact(destination.contact); + } else if (destination.releaseNotes) { strictAssert( this.#releaseNotesRecipientId == null, 'Duplicate release notes recipient' @@ -543,17 +557,17 @@ export class BackupImportStream extends Writable { // Not yet supported return; - } else if (recipient.self) { - convo = this.#fromSelf(recipient.self); - } else if (recipient.group) { - convo = await this.#fromGroup(recipient.group); - } else if (recipient.distributionList) { - await this.#fromDistributionList(recipient.distributionList); + } else if (destination.self) { + convo = this.#fromSelf(destination.self); + } else if (destination.group) { + convo = await this.#fromGroup(destination.group); + } else if (destination.distributionList) { + await this.#fromDistributionList(destination.distributionList); // Not a conversation return; - } else if (recipient.callLink) { - await this.#fromCallLink(recipientId, recipient.callLink); + } else if (destination.callLink) { + await this.#fromCallLink(recipient.id, destination.callLink); // Not a conversation return; @@ -566,25 +580,25 @@ export class BackupImportStream extends Writable { await this.#saveConversation(convo); } - this.#recipientIdToConvo.set(recipientId, convo); - } else if (frame.chat) { - await this.#fromChat(frame.chat); - } else if (frame.chatItem) { + this.#recipientIdToConvo.set(recipient.id, convo); + } else if (item.chat) { + await this.#fromChat(item.chat); + } else if (item.chatItem) { if (!aboutMe) { throw new Error( 'processFrame: Processing a chatItem frame, but no aboutMe data!' ); } - await this.#fromChatItem(frame.chatItem, { aboutMe }); - } else if (frame.stickerPack) { - await this.#fromStickerPack(frame.stickerPack); - } else if (frame.adHocCall) { - await this.#fromAdHocCall(frame.adHocCall); - } else if (frame.notificationProfile) { - await this.#fromNotificationProfile(frame.notificationProfile); - } else if (frame.chatFolder) { - await this.#fromChatFolder(frame.chatFolder); + await this.#fromChatItem(item.chatItem, { aboutMe }); + } else if (item.stickerPack) { + await this.#fromStickerPack(item.stickerPack); + } else if (item.adHocCall) { + await this.#fromAdHocCall(item.adHocCall); + } else if (item.notificationProfile) { + await this.#fromNotificationProfile(item.notificationProfile); + } else if (item.chatFolder) { + await this.#fromChatFolder(item.chatFolder); } else { log.warn( `${this.#logId}: unknown unsupported frame item ${frame.item}` @@ -770,7 +784,7 @@ export class BackupImportStream extends Writable { bioText, bioEmoji, keyTransparencyData, - }: Backups.IAccountData): Promise { + }: Backups.AccountData): Promise { strictAssert(this.#ourConversation === undefined, 'Duplicate AccountData'); const me = { ...window.ConversationController.getOurConversationOrThrow().attributes, @@ -796,7 +810,12 @@ export class BackupImportStream extends Writable { } // Same numeric value, no conversion needed - await itemStorage.put('usernameLinkColor', color ?? 0); + await itemStorage.put( + 'usernameLinkColor', + isKnownProtoEnumMember(Backups.AccountData.UsernameLink.Color, color) + ? color + : 0 + ); } if (givenName != null) { @@ -954,22 +973,20 @@ export class BackupImportStream extends Writable { const autoDownload = accountSettings?.autoDownloadSettings; if (autoDownload) { - const autoDownloadEnum = - Backups.AccountData.AutoDownloadSettings.AutoDownloadOption; await itemStorage.put('auto-download-attachment-primary', { - photos: autoDownload?.images || autoDownloadEnum.NEVER, - audio: autoDownload?.audio || autoDownloadEnum.NEVER, - videos: autoDownload?.video || autoDownloadEnum.NEVER, - documents: autoDownload?.documents || autoDownloadEnum.NEVER, + photos: parseAutoDownloadOption(autoDownload?.images), + audio: parseAutoDownloadOption(autoDownload?.audio), + videos: parseAutoDownloadOption(autoDownload?.video), + documents: parseAutoDownloadOption(autoDownload?.documents), }); } } - this.#backupTier = accountSettings?.backupTier?.toNumber(); - await itemStorage.put( - 'backupTier', - accountSettings?.backupTier?.toNumber() - ); + const backupTier = toBackupLevel(accountSettings?.backupTier); + if (backupTier != null) { + this.#backupTier = backupTier; + await itemStorage.put('backupTier', backupTier); + } await itemStorage.put( 'sealedSenderIndicators', @@ -1056,20 +1073,20 @@ export class BackupImportStream extends Writable { await this.#updateConversation(me); } - #fromSelf(self: Backups.ISelf): ConversationAttributesType { + #fromSelf(self: Backups.Self): ConversationAttributesType { strictAssert(this.#ourConversation != null, 'Missing account data'); const convo = this.#ourConversation; if (self.avatarColor != null) { convo.color = fromAvatarColor(self.avatarColor); - convo.colorFromPrimary = dropNull(self.avatarColor); + convo.colorFromPrimary = parseAvatarColorFromPrimary(self.avatarColor); } return convo; } async #fromContact( - contact: Backups.IContact + contact: Backups.Contact ): Promise { strictAssert( contact.aci != null || contact.pni != null || contact.e164 != null, @@ -1128,11 +1145,11 @@ export class BackupImportStream extends Writable { nicknameFamilyName: dropNull(contact.nickname?.family), note: dropNull(contact.note), color: fromAvatarColor(contact.avatarColor), - colorFromPrimary: dropNull(contact.avatarColor), + colorFromPrimary: parseAvatarColorFromPrimary(contact.avatarColor), }; if (serviceId != null && Bytes.isNotEmpty(contact.identityKey)) { - const verified = contact.identityState || 0; + const verified = parseIdentityState(contact.identityState); this.#identityKeys.set(serviceId, { id: serviceId, publicKey: contact.identityKey, @@ -1144,16 +1161,17 @@ export class BackupImportStream extends Writable { attrs.verified = verified; } - if (contact.notRegistered) { + const { registration } = contact; + if (registration?.notRegistered) { const timestamp = getCheckedTimestampOrUndefinedFromLong( - contact.notRegistered.unregisteredTimestamp + registration.notRegistered.unregisteredTimestamp ); attrs.discoveredUnregisteredAt = timestamp || this.#now; attrs.firstUnregisteredAt = timestamp || undefined; - } else if (!contact.registered) { + } else if (!registration?.registered) { log.error( 'contact is neither registered nor unregistered; treating as registered', - contact.registered + registration ); this.#frameErrorCount += 1; } @@ -1179,7 +1197,7 @@ export class BackupImportStream extends Writable { return attrs; } - async #fromGroup(group: Backups.IGroup): Promise { + async #fromGroup(group: Backups.Group): Promise { const { masterKey, snapshot } = group; strictAssert(masterKey != null, 'fromGroup: missing masterKey'); strictAssert(snapshot != null, 'fromGroup: missing snapshot'); @@ -1204,7 +1222,7 @@ export class BackupImportStream extends Writable { } = snapshot; const expirationTimerS = - disappearingMessagesTimer?.disappearingMessagesDuration; + disappearingMessagesTimer?.content?.disappearingMessagesDuration; let storySendMode: StorySendMode | undefined; switch (group.storySendMode) { @@ -1241,29 +1259,23 @@ export class BackupImportStream extends Writable { : undefined, remoteAvatarUrl: dropNull(avatarUrl), color: fromAvatarColor(group.avatarColor), - colorFromPrimary: dropNull(group.avatarColor), + colorFromPrimary: parseAvatarColorFromPrimary(group.avatarColor), // Snapshot - name: dropNull(title?.title)?.trim(), - description: dropNull(description?.descriptionText)?.trim(), + name: dropNull(title?.content?.title)?.trim(), + description: dropNull(description?.content?.descriptionText)?.trim(), expireTimer: expirationTimerS ? DurationInSeconds.fromSeconds(expirationTimerS) : undefined, expireTimerVersion: 1, accessControl: accessControl ? { - attributes: - dropNull(accessControl.attributes) ?? - SignalService.AccessControl.AccessRequired.UNKNOWN, - members: - dropNull(accessControl.members) ?? - SignalService.AccessControl.AccessRequired.UNKNOWN, - addFromInviteLink: - dropNull(accessControl.addFromInviteLink) ?? - SignalService.AccessControl.AccessRequired.UNKNOWN, - memberLabel: - dropNull(accessControl.memberLabel) ?? - SignalService.AccessControl.AccessRequired.UNKNOWN, + attributes: parseGroupAccessRequired(accessControl.attributes), + members: parseGroupAccessRequired(accessControl.members), + addFromInviteLink: parseGroupAccessRequired( + accessControl.addFromInviteLink + ), + memberLabel: parseGroupAccessRequired(accessControl.memberLabel), } : undefined, membersV2: members?.map( @@ -1278,7 +1290,7 @@ export class BackupImportStream extends Writable { joinedAtVersion: dropNull(joinedAtVersion) ?? 0, labelEmoji: dropNull(labelEmoji), labelString: dropNull(labelString), - role: dropNull(role) ?? SignalService.Member.Role.UNKNOWN, + role: parseGroupMemberRole(role), }; } ), @@ -1301,7 +1313,7 @@ export class BackupImportStream extends Writable { return { serviceId, - role: dropNull(role) ?? SignalService.Member.Role.UNKNOWN, + role: parseGroupMemberRole(role), addedByUserId: fromAciObject(Aci.fromUuidBytes(addedByUserId)), timestamp: timestamp != null ? getCheckedTimestampFromLong(timestamp) : 0, @@ -1353,7 +1365,7 @@ export class BackupImportStream extends Writable { } async #fromDistributionList( - listItem: Backups.IDistributionListItem + listItem: Backups.DistributionListItem ): Promise { strictAssert( Bytes.isNotEmpty(listItem.distributionId), @@ -1372,8 +1384,12 @@ export class BackupImportStream extends Writable { }; let result: StoryDistributionWithMembersType; - if (listItem.deletionTimestamp == null) { - const { distributionList: list } = listItem; + + const { item } = listItem; + strictAssert(item, 'Missing DistributionListItem.item'); + + if (item.deletionTimestamp == null) { + const { distributionList: list } = item; strictAssert( list != null, 'Distribution list is either present or deleted' @@ -1386,6 +1402,10 @@ export class BackupImportStream extends Writable { let isBlockList: boolean; const { PrivacyMode } = Backups.DistributionList; + strictAssert( + isKnownProtoEnumMember(PrivacyMode, list.privacyMode), + `Unknown privacy mode for distribution list: ${list.privacyMode}` + ); switch (list.privacyMode) { case PrivacyMode.ALL: strictAssert( @@ -1421,7 +1441,7 @@ export class BackupImportStream extends Writable { allowsReplies: list.allowReplies === true, isBlockList, members: (list.memberRecipientIds || []).map(recipientId => { - const convo = this.#recipientIdToConvo.get(recipientId.toNumber()); + const convo = this.#recipientIdToConvo.get(recipientId); strictAssert(convo != null, 'Missing story distribution list member'); strictAssert( convo.serviceId, @@ -1440,9 +1460,7 @@ export class BackupImportStream extends Writable { isBlockList: false, members: [], - deletedAtTimestamp: getCheckedTimestampFromLong( - listItem.deletionTimestamp - ), + deletedAtTimestamp: getCheckedTimestampFromLong(item.deletionTimestamp), }; } @@ -1450,8 +1468,8 @@ export class BackupImportStream extends Writable { } async #fromCallLink( - recipientId: number, - callLinkProto: Backups.ICallLink + recipientId: bigint, + callLinkProto: Backups.CallLink ): Promise { const { rootKey: rootKeyBytes, @@ -1489,12 +1507,12 @@ export class BackupImportStream extends Writable { await DataWriter.insertCallLink(callLink); } - async #fromChat(chat: Backups.IChat): Promise { + async #fromChat(chat: Backups.Chat): Promise { strictAssert(chat.id != null, 'chat must have an id'); strictAssert(chat.recipientId != null, 'chat must have a recipientId'); // Drop release notes chat - if (this.#releaseNotesRecipientId?.eq(chat.recipientId)) { + if (this.#releaseNotesRecipientId === chat.recipientId) { strictAssert( this.#releaseNotesChatId == null, 'Duplicate release notes chat' @@ -1503,12 +1521,10 @@ export class BackupImportStream extends Writable { return; } - const conversation = this.#recipientIdToConvo.get( - chat.recipientId.toNumber() - ); + const conversation = this.#recipientIdToConvo.get(chat.recipientId); strictAssert(conversation !== undefined, 'unknown conversation'); - this.#chatIdToConvo.set(chat.id.toNumber(), conversation); + this.#chatIdToConvo.set(chat.id, conversation); if (isTestEnvironment(getEnvironment())) { conversation.test_chatFrameImportedFromBackup = true; @@ -1518,14 +1534,14 @@ export class BackupImportStream extends Writable { conversation.isPinned = (chat.pinnedOrder || 0) !== 0; conversation.expireTimer = - chat.expirationTimerMs && !chat.expirationTimerMs.isZero() - ? DurationInSeconds.fromMillis(chat.expirationTimerMs.toNumber()) + chat.expirationTimerMs && chat.expirationTimerMs !== 0n + ? DurationInSeconds.fromMillis(toNumber(chat.expirationTimerMs)) : undefined; conversation.expireTimerVersion = chat.expireTimerVersion || 1; if ( chat.muteUntilMs != null && - chat.muteUntilMs.toNumber() >= MAX_SAFE_DATE + toNumber(chat.muteUntilMs) >= MAX_SAFE_DATE ) { // Muted forever conversation.muteExpiresAt = Number.MAX_SAFE_INTEGER; @@ -1570,12 +1586,14 @@ export class BackupImportStream extends Writable { } async #fromChatItem( - item: Backups.IChatItem, + chatItem: Backups.ChatItem, options: { aboutMe: AboutMe } ): Promise { const { aboutMe } = options; - const timestamp = getCheckedTimestampOrUndefinedFromLong(item?.dateSent); + const timestamp = getCheckedTimestampOrUndefinedFromLong( + chatItem?.dateSent + ); const logId = `fromChatItem(${timestamp})`; strictAssert( @@ -1583,34 +1601,41 @@ export class BackupImportStream extends Writable { `${logId}: AccountData missing` ); - strictAssert(item.chatId != null, `${logId}: must have a chatId`); - strictAssert(item.dateSent != null, `${logId}: must have a dateSent`); + strictAssert(chatItem.chatId != null, `${logId}: must have a chatId`); + strictAssert(chatItem.dateSent != null, `${logId}: must have a dateSent`); strictAssert(timestamp, `${logId}: must have a timestamp`); - if (this.#releaseNotesChatId?.eq(item.chatId)) { + if (this.#releaseNotesChatId === chatItem.chatId) { // Drop release notes messages return; } - const chatConvo = this.#chatIdToConvo.get(item.chatId.toNumber()); + const chatConvo = this.#chatIdToConvo.get(chatItem.chatId); strictAssert( chatConvo !== undefined, `${logId}: chat conversation not found` ); - const authorConvo = item.authorId - ? this.#recipientIdToConvo.get(item.authorId.toNumber()) - : undefined; + const authorConvo = + chatItem.authorId != null + ? this.#recipientIdToConvo.get(chatItem.authorId) + : undefined; + + const { directionalDetails } = chatItem; + strictAssert( + directionalDetails, + `${logId}: message must have directionalDetails` + ); const { patch: directionDetails, newActiveAt, unread, - } = this.#fromDirectionDetails(item, timestamp); + } = this.#fromDirectionDetails(chatItem, directionalDetails, timestamp); if ( newActiveAt != null && - this.#shouldChatItemAffectChatListPresence(item) + this.#shouldChatItemAffectChatListPresence(chatItem) ) { chatConvo.active_at = newActiveAt; } @@ -1620,11 +1645,11 @@ export class BackupImportStream extends Writable { } const expirationStartTimestamp = getCheckedTimestampOrUndefinedFromLong( - item.expireStartDate + chatItem.expireStartDate ); const expireTimer = - item.expiresInMs && !item.expiresInMs.isZero() - ? DurationInSeconds.fromMillis(item.expiresInMs.toNumber()) + chatItem.expiresInMs && chatItem.expiresInMs !== 0n + ? DurationInSeconds.fromMillis(toNumber(chatItem.expiresInMs)) : undefined; const expirationTimestamp = calculateExpirationTimestamp({ @@ -1644,26 +1669,29 @@ export class BackupImportStream extends Writable { source: authorConvo?.e164, sourceServiceId: authorConvo?.serviceId, timestamp, - type: item.outgoing != null ? 'outgoing' : 'incoming', + type: directionalDetails.outgoing != null ? 'outgoing' : 'incoming', expirationStartTimestamp, expireTimer, - sms: item.sms === true ? true : undefined, + sms: chatItem.sms === true ? true : undefined, ...directionDetails, }; const additionalMessages: Array = []; - if (item.incoming) { + if (directionalDetails.incoming) { strictAssert( authorConvo && this.#ourConversation.id !== authorConvo?.id, `${logId}: message with incoming field must be incoming` ); - } else if (item.outgoing) { + } else if (directionalDetails.outgoing) { strictAssert( authorConvo && this.#ourConversation.id === authorConvo?.id, `${logId}: outgoing message must have outgoing field` ); } + const { item } = chatItem; + strictAssert(item, `${logId}: chatItem must have item`); + if (item.standardMessage) { attributes = { ...attributes, @@ -1675,12 +1703,15 @@ export class BackupImportStream extends Writable { } else if (item.viewOnceMessage) { attributes = { ...attributes, - ...(await this.#fromViewOnceMessage(item)), + ...(await this.#fromViewOnceMessage(chatItem, item.viewOnceMessage)), }; } else if (item.directStoryReplyMessage) { - strictAssert(item.directionless == null, 'reply cannot be directionless'); + strictAssert( + directionalDetails.directionless == null, + 'reply cannot be directionless' + ); let storyAuthorAci: AciString | undefined; - if (item.incoming) { + if (directionalDetails.incoming) { strictAssert(this.#aboutMe?.aci, 'about me must exist'); storyAuthorAci = this.#aboutMe.aci; } else { @@ -1712,13 +1743,11 @@ export class BackupImportStream extends Writable { poll.options?.forEach((option, optionIndex) => { option.votes?.forEach(vote => { - if (!vote.voterId) { + if (vote.voterId == null) { return; } - const conversation = this.#recipientIdToConvo.get( - vote.voterId.toNumber() - ); + const conversation = this.#recipientIdToConvo.get(vote.voterId); if (!conversation) { log.warn(`${logId}: Poll vote has unknown voterId ${vote.voterId}`); return; @@ -1750,12 +1779,12 @@ export class BackupImportStream extends Writable { options: poll.options?.map(option => option.option ?? '') ?? [], allowMultiple: poll.allowMultiple ?? false, votes: votes.length > 0 ? votes : undefined, - terminatedAt: poll.hasEnded ? Number(item.dateSent) : undefined, + terminatedAt: poll.hasEnded ? toNumber(chatItem.dateSent) : undefined, }, reactions: this.#fromReactions(poll.reactions), }; } else { - const result = await this.#fromNonBubbleChatItem(item, { + const result = await this.#fromNonBubbleChatItem(chatItem, { aboutMe, author: authorConvo, conversation: chatConvo, @@ -1787,7 +1816,7 @@ export class BackupImportStream extends Writable { }); } - if (item.revisions?.length) { + if (chatItem.revisions?.length) { strictAssert( item.standardMessage || item.directStoryReplyMessage, `${logId}: Only standard or story reply message can have revisions` @@ -1795,7 +1824,7 @@ export class BackupImportStream extends Writable { const history = await this.#fromRevisions({ mainMessage: attributes, - revisions: item.revisions, + revisions: chatItem.revisions, logId, }); attributes.editHistory = history; @@ -1824,25 +1853,25 @@ export class BackupImportStream extends Writable { ...additionalMessages.map(additional => this.#saveMessage(additional)), ]); - if (item.outgoing != null) { + if (directionalDetails.outgoing != null) { chatConvo.sentMessageCount = (chatConvo.sentMessageCount ?? 0) + 1; - } else if (item.incoming != null) { + } else if (directionalDetails.incoming != null) { chatConvo.messageCount = (chatConvo.messageCount ?? 0) + 1; } - if (item.pinDetails != null) { - strictAssert( - item.pinDetails.pinnedAtTimestamp != null, - 'pinDetails: Missing pinnedAtTimestamp' - ); - const pinnedAt = item.pinDetails.pinnedAtTimestamp.toNumber(); + if (chatItem.pinDetails != null) { + const { pinExpiry, pinnedAtTimestamp } = chatItem.pinDetails; + strictAssert(pinnedAtTimestamp, 'Missing PinDetails.pinnedAtTimestamp'); + strictAssert(pinExpiry, 'Missing PinDetails.pinExpiry'); + + const pinnedAt = toNumber(pinnedAtTimestamp); let expiresAt: number | null; - if (item.pinDetails.pinExpiresAtTimestamp != null) { - expiresAt = item.pinDetails.pinExpiresAtTimestamp.toNumber(); + if (pinExpiry.pinExpiresAtTimestamp != null) { + expiresAt = toNumber(pinExpiry.pinExpiresAtTimestamp); } else { strictAssert( - item.pinDetails.pinNeverExpires === true, + pinExpiry.pinNeverExpires === true, 'pinDetails: pinNeverExpires should be true if theres no pinExpiresAtTimestamp' ); expiresAt = null; @@ -1860,21 +1889,23 @@ export class BackupImportStream extends Writable { } #fromDirectionDetails( - item: Backups.IChatItem, + item: Backups.ChatItem, + directionalDetails: NonNullable, timestamp: number ): { patch: Partial; newActiveAt?: number; unread?: boolean; } { - const { outgoing, incoming, directionless } = item; + const { outgoing, incoming, directionless } = directionalDetails; if (outgoing) { const sendStateByConversationId: SendStateByConversationId = {}; const unidentifiedDeliveries = new Array(); const errors = new Array(); - let sendStatuses = outgoing.sendStatus; + let sendStatuses: Array = + outgoing.sendStatus; if (!sendStatuses?.length) { // TODO: DESKTOP-8089 // If this outgoing message was not sent to anyone, we add ourselves to @@ -1883,7 +1914,7 @@ export class BackupImportStream extends Writable { sendStatuses = [ { recipientId: item.authorId, - read: new Backups.SendStatus.Read(), + deliveryStatus: { read: { sealedSender: null } }, timestamp: item.dateSent, }, ]; @@ -1891,49 +1922,56 @@ export class BackupImportStream extends Writable { for (const status of sendStatuses) { strictAssert( - status.recipientId, + status.recipientId != null, 'sendStatus recipient must have an id' ); - const target = this.#recipientIdToConvo.get( - status.recipientId.toNumber() - ); + const target = this.#recipientIdToConvo.get(status.recipientId); strictAssert( target !== undefined, 'status target conversation not found' ); const { serviceId } = target; + const { deliveryStatus } = status; + strictAssert(deliveryStatus, 'sendStatus must have a deliveryStatus'); let sendStatus: SendStatus; - if (status.pending) { + if (deliveryStatus.pending) { sendStatus = SendStatus.Pending; - } else if (status.sent) { + } else if (deliveryStatus.sent) { sendStatus = SendStatus.Sent; - if (serviceId && status.sent.sealedSender) { + if (serviceId && deliveryStatus.sent.sealedSender) { unidentifiedDeliveries.push(serviceId); } - } else if (status.delivered) { + } else if (deliveryStatus.delivered) { sendStatus = SendStatus.Delivered; - if (serviceId && status.delivered.sealedSender) { + if (serviceId && deliveryStatus.delivered.sealedSender) { unidentifiedDeliveries.push(serviceId); } - } else if (status.read) { + } else if (deliveryStatus.read) { sendStatus = SendStatus.Read; - if (serviceId && status.read.sealedSender) { + if (serviceId && deliveryStatus.read.sealedSender) { unidentifiedDeliveries.push(serviceId); } - } else if (status.viewed) { + } else if (deliveryStatus.viewed) { sendStatus = SendStatus.Viewed; - if (serviceId && status.viewed.sealedSender) { + if (serviceId && deliveryStatus.viewed.sealedSender) { unidentifiedDeliveries.push(serviceId); } - } else if (status.failed) { + } else if (deliveryStatus.failed) { sendStatus = SendStatus.Failed; strictAssert( - status.failed.reason != null, + deliveryStatus.failed.reason != null, 'Failure reason must exist' ); - switch (status.failed.reason) { + strictAssert( + isKnownProtoEnumMember( + Backups.SendStatus.Failed.FailureReason, + deliveryStatus.failed.reason + ), + `Unknown failure reason: ${deliveryStatus.failed.reason}` + ); + switch (deliveryStatus.failed.reason) { case Backups.SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH: errors.push({ serviceId, @@ -1958,10 +1996,10 @@ export class BackupImportStream extends Writable { }); break; default: - throw missingCaseError(status.failed.reason); + throw missingCaseError(deliveryStatus.failed.reason); } // Desktop does not keep track of users we did not attempt to send to - } else if (status.skipped) { + } else if (deliveryStatus.skipped) { sendStatus = SendStatus.Skipped; } else { log.error( @@ -1975,7 +2013,7 @@ export class BackupImportStream extends Writable { sendStateByConversationId[target.id] = { status: sendStatus, updatedAt: - status.timestamp != null && !status.timestamp.isZero() + status.timestamp != null && status.timestamp !== 0n ? getCheckedTimestampFromLong(status.timestamp) : undefined, }; @@ -2051,32 +2089,46 @@ export class BackupImportStream extends Writable { * * iOS list: /main/SignalServiceKit/Messages/Interactions/TSInteraction.swift */ - #shouldChatItemAffectChatListPresence(item: Backups.IChatItem): boolean { + #shouldChatItemAffectChatListPresence(chatItem: Backups.ChatItem): boolean { + const { item } = chatItem; + strictAssert(item, 'Missing chatItem.item'); + if (!item.updateMessage) { return true; } + const { update } = item.updateMessage; + strictAssert(update, 'Missing updateMessage.update'); + if ( - item.updateMessage.profileChange || - item.updateMessage.learnedProfileChange || - item.updateMessage.sessionSwitchover || - item.updateMessage.threadMerge + update.profileChange || + update.learnedProfileChange || + update.sessionSwitchover || + update.threadMerge ) { return false; } if ( - item.updateMessage.groupChange?.updates?.every( - update => - Boolean(update.groupMemberLeftUpdate) || - Boolean(update.groupV2MigrationUpdate) + update.groupChange?.updates?.every( + groupUpdate => + Boolean(groupUpdate.update?.groupMemberLeftUpdate) || + Boolean(groupUpdate.update?.groupV2MigrationUpdate) ) ) { return false; } - if (item.updateMessage.simpleUpdate) { - switch (item.updateMessage.simpleUpdate.type) { + if (update.simpleUpdate) { + strictAssert( + isKnownProtoEnumMember( + Backups.SimpleChatUpdate.Type, + update.simpleUpdate.type + ), + `Unknown SimpleChatUpdate.Type: ${update.simpleUpdate.type}` + ); + + switch (update.simpleUpdate.type) { case Backups.SimpleChatUpdate.Type.IDENTITY_UPDATE: case Backups.SimpleChatUpdate.Type.CHANGE_NUMBER: case Backups.SimpleChatUpdate.Type.MESSAGE_REQUEST_ACCEPTED: @@ -2101,7 +2153,7 @@ export class BackupImportStream extends Writable { case Backups.SimpleChatUpdate.Type.UNSUPPORTED_PROTOCOL_MESSAGE: return true; default: - throw missingCaseError(item.updateMessage.simpleUpdate.type); + throw missingCaseError(update.simpleUpdate.type); } } @@ -2113,7 +2165,7 @@ export class BackupImportStream extends Writable { data, }: { logId: string; - data: Backups.IStandardMessage; + data: Backups.StandardMessage; }): Promise> { return { // We don't want to trim if we'll be downloading a body attachment; we might @@ -2159,7 +2211,7 @@ export class BackupImportStream extends Writable { }: { logId: string; body: string | null | undefined; - previews: Array; + previews: Array; }): Array { const urlsInBody = LinkPreview.findLinks(body ?? ''); return previews @@ -2189,10 +2241,10 @@ export class BackupImportStream extends Writable { } async #fromViewOnceMessage( - item: Backups.IChatItem + item: Backups.ChatItem, + viewOnceMessage: Backups.ViewOnceMessage ): Promise> { - const { incoming, viewOnceMessage } = item; - strictAssert(viewOnceMessage, 'view once message must not be null'); + const incoming = item.directionalDetails?.incoming; const { attachment, reactions } = viewOnceMessage; const result: Partial = { @@ -2222,10 +2274,15 @@ export class BackupImportStream extends Writable { } #fromDirectStoryReplyMessage( - directStoryReplyMessage: Backups.IDirectStoryReplyMessage, + directStoryReplyMessage: Backups.DirectStoryReplyMessage, storyAuthorAci: AciString ): Partial { - const { reactions, textReply, emoji } = directStoryReplyMessage; + strictAssert( + directStoryReplyMessage.reply, + 'directStoryReplyMessage.reply is missing' + ); + const { textReply, emoji } = directStoryReplyMessage.reply; + const { reactions } = directStoryReplyMessage; const result: Partial = { reactions: this.#fromReactions(reactions), @@ -2257,9 +2314,9 @@ export class BackupImportStream extends Writable { } async #fromDirectStoryReplyRevision( - revision: Backups.IDirectStoryReplyMessage + revision: Backups.DirectStoryReplyMessage ): Promise> { - const { textReply } = revision; + const textReply = revision.reply?.textReply; if (!textReply) { return {}; @@ -2280,17 +2337,22 @@ export class BackupImportStream extends Writable { logId, }: { mainMessage: MessageAttributesType; - revisions: ReadonlyArray; + revisions: ReadonlyArray; logId: string; }): Promise> { const result = await Promise.all( revisions .map(async rev => { strictAssert( - rev.standardMessage || rev.directStoryReplyMessage, + rev.item?.standardMessage || rev.item?.directStoryReplyMessage, 'Edit history on a message that does not support revisions' ); + strictAssert( + rev.directionalDetails, + 'Edit history on a message that does not have directional details' + ); + const timestamp = getCheckedTimestampFromLong(rev.dateSent); const { @@ -2302,7 +2364,11 @@ export class BackupImportStream extends Writable { readStatus, unidentifiedDeliveryReceived, }, - } = this.#fromDirectionDetails(rev, timestamp); + } = this.#fromDirectionDetails( + rev, + rev.directionalDetails, + timestamp + ); const commonFields = { timestamp, @@ -2315,20 +2381,20 @@ export class BackupImportStream extends Writable { unidentifiedDeliveryReceived, }; - if (rev.standardMessage) { + if (rev.item?.standardMessage) { return { ...(await this.#fromStandardMessage({ logId, - data: rev.standardMessage, + data: rev.item?.standardMessage, })), ...commonFields, }; } - if (rev.directStoryReplyMessage) { + if (rev.item?.directStoryReplyMessage) { return { ...(await this.#fromDirectStoryReplyRevision( - rev.directStoryReplyMessage + rev.item?.directStoryReplyMessage )), ...commonFields, }; @@ -2364,10 +2430,10 @@ export class BackupImportStream extends Writable { return result; } - async #fromQuote(quote: Backups.IQuote): Promise { + async #fromQuote(quote: Backups.Quote): Promise { strictAssert(quote.authorId != null, 'quote must have an authorId'); - const authorConvo = this.#recipientIdToConvo.get(quote.authorId.toNumber()); + const authorConvo = this.#recipientIdToConvo.get(quote.authorId); strictAssert(authorConvo !== undefined, 'author conversation not found'); return { @@ -2401,7 +2467,7 @@ export class BackupImportStream extends Writable { } #fromBodyRanges( - text: Backups.IText | null | undefined + text: Backups.Text | null | undefined ): ReadonlyArray | undefined { if (text == null) { return undefined; @@ -2412,17 +2478,36 @@ export class BackupImportStream extends Writable { } return filterAndClean( - bodyRanges.map(range => ({ - ...range, - mentionAci: range.mentionAci - ? Aci.parseFromServiceIdBinary(range.mentionAci).getServiceIdString() - : undefined, - })) + bodyRanges.map((range): RawBodyRange => { + const { start, length, associatedValue } = range; + strictAssert(associatedValue, 'Misisng BodyRange.associatedValue'); + if (associatedValue.mentionAci) { + const mentionAci: AciString = fromAciObject( + Aci.parseFromServiceIdBinary(associatedValue.mentionAci) + ); + return { start, length, mentionAci }; + } + if (associatedValue.style != null) { + strictAssert( + isKnownProtoEnumMember( + Backups.BodyRange.Style, + associatedValue.style + ), + 'Unexpected non-enum value' + ); + return { + start, + length, + style: associatedValue.style, + }; + } + throw missingCaseError(associatedValue); + }) ); } #fromReactions( - reactions: ReadonlyArray | null | undefined + reactions: ReadonlyArray | null | undefined ): Array | undefined { if (!reactions?.length) { return undefined; @@ -2430,8 +2515,11 @@ export class BackupImportStream extends Writable { return reactions .slice() .sort((a, b) => { - if (a.sortOrder && b.sortOrder) { - return a.sortOrder.comp(b.sortOrder); + if (a.sortOrder < b.sortOrder) { + return -1; + } + if (a.sortOrder > b.sortOrder) { + return 1; } return 0; }) @@ -2443,7 +2531,7 @@ export class BackupImportStream extends Writable { 'reaction must have a sentTimestamp' ); - const authorConvo = this.#recipientIdToConvo.get(authorId.toNumber()); + const authorConvo = this.#recipientIdToConvo.get(authorId); strictAssert( authorConvo !== undefined, 'author conversation not found' @@ -2459,7 +2547,7 @@ export class BackupImportStream extends Writable { } async #fromNonBubbleChatItem( - chatItem: Backups.IChatItem, + chatItem: Backups.ChatItem, options: { aboutMe: AboutMe; author?: ConversationAttributesType; @@ -2470,11 +2558,14 @@ export class BackupImportStream extends Writable { const { timestamp } = options; const logId = `fromChatItemToNonBubble(${timestamp})`; - if (chatItem.standardMessage) { + const { item } = chatItem; + strictAssert(item, 'Missing ChatItem.item'); + + if (item.standardMessage) { throw new Error(`${logId}: Got chat item with standardMessage set!`); } - if (chatItem.contactMessage) { - const { contact: details } = chatItem.contactMessage; + if (item.contactMessage) { + const { contact: details } = item.contactMessage; strictAssert(details != null, 'contactMessage must have a contact'); const { avatar, name, number, email, address, organization } = details; @@ -2562,18 +2653,18 @@ export class BackupImportStream extends Writable { : undefined, }, ], - reactions: this.#fromReactions(chatItem.contactMessage.reactions), + reactions: this.#fromReactions(item.contactMessage.reactions), }, additionalMessages: [], }; } - if (chatItem.adminDeletedMessage) { + if (item.adminDeletedMessage) { strictAssert( - chatItem.adminDeletedMessage.adminId != null, + item.adminDeletedMessage.adminId != null, 'adminDeletedMessage: Missing adminId' ); const adminConversation = this.#recipientIdToConvo.get( - chatItem.adminDeletedMessage.adminId.toNumber() + item.adminDeletedMessage.adminId ); strictAssert( adminConversation != null, @@ -2593,7 +2684,7 @@ export class BackupImportStream extends Writable { additionalMessages: [], }; } - if (chatItem.remoteDeletedMessage) { + if (item.remoteDeletedMessage) { return { message: { isErased: true, @@ -2602,16 +2693,16 @@ export class BackupImportStream extends Writable { additionalMessages: [], }; } - if (chatItem.stickerMessage) { + if (item.stickerMessage) { strictAssert( - chatItem.stickerMessage.sticker != null, + item.stickerMessage.sticker != null, 'stickerMessage must have a sticker' ); const { stickerMessage: { sticker: { emoji, packId, packKey, stickerId, data }, }, - } = chatItem; + } = item; strictAssert( packId?.length === STICKERPACK_ID_BYTE_LEN, 'stickerMessage must have a valid pack id' @@ -2633,13 +2724,13 @@ export class BackupImportStream extends Writable { ? convertFilePointerToAttachment(data, this.options) : undefined, }, - reactions: this.#fromReactions(chatItem.stickerMessage.reactions), + reactions: this.#fromReactions(item.stickerMessage.reactions), }, additionalMessages: [], }; } - if (chatItem.paymentNotification) { - const { paymentNotification: notification } = chatItem; + if (item.paymentNotification) { + const { paymentNotification: notification } = item; return { message: { payment: { @@ -2651,7 +2742,7 @@ export class BackupImportStream extends Writable { ? Bytes.toBase64( Backups.PaymentNotification.TransactionDetails.encode( notification.transactionDetails - ).finish() + ) ) : undefined, }, @@ -2659,8 +2750,8 @@ export class BackupImportStream extends Writable { additionalMessages: [], }; } - if (chatItem.giftBadge) { - const { giftBadge } = chatItem; + if (item.giftBadge) { + const { giftBadge } = item; if (giftBadge.state === Backups.GiftBadge.State.FAILED) { return { message: { @@ -2712,15 +2803,15 @@ export class BackupImportStream extends Writable { additionalMessages: [], }; } - if (chatItem.updateMessage) { - return this.#fromChatItemUpdateMessage(chatItem.updateMessage, options); + if (item.updateMessage) { + return this.#fromChatItemUpdateMessage(item.updateMessage, options); } throw new Error(`${logId}: Message was missing all five message types`); } async #fromChatItemUpdateMessage( - updateMessage: Backups.IChatUpdateMessage, + updateMessage: Backups.ChatUpdateMessage, options: { aboutMe: AboutMe; author?: ConversationAttributesType; @@ -2730,12 +2821,15 @@ export class BackupImportStream extends Writable { ): Promise { const { aboutMe, author, conversation } = options; - if (updateMessage.groupChange) { - return this.#fromGroupUpdateMessage(updateMessage.groupChange, options); + const { update } = updateMessage; + strictAssert(update, 'Missing ChatUpdateMessage.update'); + + if (update.groupChange) { + return this.#fromGroupUpdateMessage(update.groupChange, options); } - if (updateMessage.expirationTimerChange) { - const { expiresInMs } = updateMessage.expirationTimerChange; + if (update.expirationTimerChange) { + const { expiresInMs } = update.expirationTimerChange; let sourceServiceId = author?.serviceId; let source = author?.e164; @@ -2744,7 +2838,7 @@ export class BackupImportStream extends Writable { source = aboutMe.e164; } const expireTimer = DurationInSeconds.fromMillis( - expiresInMs?.toNumber() ?? 0 + toNumber(expiresInMs) ?? 0 ); return { @@ -2762,9 +2856,9 @@ export class BackupImportStream extends Writable { }; } - if (updateMessage.simpleUpdate) { + if (update.simpleUpdate) { const message = await this.#fromSimpleUpdateMessage( - updateMessage.simpleUpdate, + update.simpleUpdate, options ); @@ -2778,8 +2872,8 @@ export class BackupImportStream extends Writable { }; } - if (updateMessage.profileChange) { - const { newName, previousName: oldName } = updateMessage.profileChange; + if (update.profileChange) { + const { newName, previousName: oldName } = update.profileChange; strictAssert(newName != null, 'profileChange must have a new name'); strictAssert(oldName != null, 'profileChange must have an old name'); return { @@ -2796,8 +2890,8 @@ export class BackupImportStream extends Writable { }; } - if (updateMessage.learnedProfileChange) { - const { e164, username } = updateMessage.learnedProfileChange; + if (update.learnedProfileChange) { + const { e164, username } = update.learnedProfileChange.previousName ?? {}; if (e164 == null && username == null) { log.error( `${options.timestamp}: learnedProfileChange had no previous e164 or username` @@ -2810,7 +2904,7 @@ export class BackupImportStream extends Writable { titleTransition: { renderInfo: { type: 'private', - e164: e164 && !e164.isZero() ? `+${e164}` : undefined, + e164: e164 && e164 !== 0n ? `+${e164}` : undefined, username: dropNull(username), }, }, @@ -2819,8 +2913,8 @@ export class BackupImportStream extends Writable { }; } - if (updateMessage.threadMerge) { - const { previousE164 } = updateMessage.threadMerge; + if (update.threadMerge) { + const { previousE164 } = update.threadMerge; strictAssert(previousE164 != null, 'threadMerge must have an old e164'); return { message: { @@ -2836,8 +2930,8 @@ export class BackupImportStream extends Writable { }; } - if (updateMessage.sessionSwitchover) { - const { e164 } = updateMessage.sessionSwitchover; + if (update.sessionSwitchover) { + const { e164 } = update.sessionSwitchover; strictAssert(e164 != null, 'sessionSwitchover must have an old e164'); return { message: { @@ -2850,7 +2944,7 @@ export class BackupImportStream extends Writable { }; } - if (updateMessage.groupCall) { + if (update.groupCall) { const { groupId } = conversation; if (!isGroup(conversation)) { @@ -2861,27 +2955,27 @@ export class BackupImportStream extends Writable { } const { - callId: callIdLong, + callId: callIdInt, state, - ringerRecipientId: ringerRecipientIdLong, - startedCallRecipientId: startedCallRecipientIdLong, + ringerRecipientId, + startedCallRecipientId, startedCallTimestamp, endedCallTimestamp, read, - } = updateMessage.groupCall; + } = update.groupCall; - const ringerRecipientId = ringerRecipientIdLong?.toNumber(); - const startedCallRecipientId = startedCallRecipientIdLong?.toNumber(); - const ringer = isNumber(ringerRecipientId) - ? this.#recipientIdToConvo.get(ringerRecipientId) - : undefined; - const startedBy = isNumber(startedCallRecipientId) - ? this.#recipientIdToConvo.get(startedCallRecipientId) - : undefined; + const ringer = + ringerRecipientId != null + ? this.#recipientIdToConvo.get(ringerRecipientId) + : undefined; + const startedBy = + startedCallRecipientId != null + ? this.#recipientIdToConvo.get(startedCallRecipientId) + : undefined; let callId: string; - if (callIdLong?.toNumber()) { - callId = callIdLong.toString(); + if (callIdInt != null && callIdInt !== 0n) { + callId = callIdInt.toString(); } else { // Legacy calls may not have a callId, so we generate one locally callId = generateUuid(); @@ -2923,19 +3017,19 @@ export class BackupImportStream extends Writable { }; } - if (updateMessage.individualCall) { + if (update.individualCall) { const { - callId: callIdLong, + callId: callIdInt, type, direction: protoDirection, state, startedCallTimestamp, read, - } = updateMessage.individualCall; + } = update.individualCall; let callId: string; - if (callIdLong?.toNumber()) { - callId = callIdLong.toString(); + if (callIdInt != null && callIdInt !== 0n) { + callId = callIdInt.toString(); } else { // Legacy calls may not have a callId, so we generate one locally callId = generateUuid(); @@ -2983,13 +3077,13 @@ export class BackupImportStream extends Writable { }; } - if (updateMessage.pinMessage) { + if (update.pinMessage) { strictAssert( - updateMessage.pinMessage.authorId != null, + update.pinMessage.authorId != null, 'pinMessage: Missing authorId' ); const targetAuthor = this.#recipientIdToConvo.get( - updateMessage.pinMessage.authorId.toNumber() + update.pinMessage.authorId ); strictAssert(targetAuthor != null, 'pinMessage: Missing target author'); const targetAuthorAci = targetAuthor.serviceId; @@ -2999,11 +3093,12 @@ export class BackupImportStream extends Writable { ); strictAssert( - updateMessage.pinMessage.targetSentTimestamp != null, + update.pinMessage.targetSentTimestamp != null, 'pinMessage: Missing targetSentTimestamp' ); - const targetSentTimestamp = - updateMessage.pinMessage.targetSentTimestamp.toNumber(); + const targetSentTimestamp = toNumber( + update.pinMessage.targetSentTimestamp + ); return { message: { @@ -3017,7 +3112,7 @@ export class BackupImportStream extends Writable { }; } - if (updateMessage.pollTerminate) { + if (update.pollTerminate) { // TODO (DESKTOP-9282) log.warn('Skipping pollTerminate update (not yet supported)'); return SKIP; @@ -3027,7 +3122,7 @@ export class BackupImportStream extends Writable { } async #fromGroupUpdateMessage( - groupChange: Backups.IGroupChangeChatUpdate, + groupChange: Backups.GroupChangeChatUpdate, options: { aboutMe: AboutMe; author?: ConversationAttributesType; @@ -3056,7 +3151,10 @@ export class BackupImportStream extends Writable { let openApprovalServiceId: ServiceIdString | undefined; let openBounceServiceId: ServiceIdString | undefined; - updates?.forEach(update => { + updates?.forEach(updateItem => { + const { update } = updateItem; + strictAssert(update, 'Missing GroupChangeChatUpdate.Update.update'); + if (update.genericGroupUpdate) { const { updaterAci } = update.genericGroupUpdate; if (updaterAci) { @@ -3118,9 +3216,7 @@ export class BackupImportStream extends Writable { } details.push({ type: 'access-members', - newPrivilege: - dropNull(accessLevel) ?? - SignalService.AccessControl.AccessRequired.UNKNOWN, + newPrivilege: parseGroupV2AccessLevel(accessLevel), }); } if (update.groupMemberLabelAccessLevelChangeUpdate) { @@ -3131,9 +3227,7 @@ export class BackupImportStream extends Writable { } details.push({ type: 'access-member-label', - newPrivilege: - dropNull(accessLevel) ?? - SignalService.AccessControl.AccessRequired.UNKNOWN, + newPrivilege: parseGroupV2AccessLevel(accessLevel), }); } if (update.groupAttributesAccessLevelChangeUpdate) { @@ -3144,9 +3238,7 @@ export class BackupImportStream extends Writable { } details.push({ type: 'access-attributes', - newPrivilege: - dropNull(accessLevel) ?? - SignalService.AccessControl.AccessRequired.UNKNOWN, + newPrivilege: parseGroupV2AccessLevel(accessLevel), }); } if (update.groupAnnouncementOnlyChangeUpdate) { @@ -3567,7 +3659,7 @@ export class BackupImportStream extends Writable { } const expireTimer = expiresInMs - ? DurationInSeconds.fromMillis(expiresInMs.toNumber()) + ? DurationInSeconds.fromMillis(toNumber(expiresInMs)) : undefined; additionalMessages.push({ type: 'timer-notification', @@ -3639,7 +3731,7 @@ export class BackupImportStream extends Writable { } async #fromSimpleUpdateMessage( - simpleUpdate: Backups.ISimpleChatUpdate, + simpleUpdate: Backups.SimpleChatUpdate, { author, conversation, @@ -3740,7 +3832,7 @@ export class BackupImportStream extends Writable { async #fromStickerPack({ packId: packIdBytes, packKey: packKeyBytes, - }: Backups.IStickerPack): Promise { + }: Backups.StickerPack): Promise { strictAssert( packIdBytes?.length === STICKERPACK_ID_BYTE_LEN, 'Sticker pack must have a valid pack id' @@ -3759,12 +3851,12 @@ export class BackupImportStream extends Writable { async #fromAdHocCall({ callId: callIdLong, - recipientId: recipientIdLong, + recipientId, state, callTimestamp, - }: Backups.IAdHocCall): Promise { + }: Backups.AdHocCall): Promise { let callId: string; - if (callIdLong?.toNumber()) { + if (toNumber(callIdLong)) { callId = callIdLong.toString(); } else { // Legacy calls may not have a callId, so we generate one locally @@ -3774,9 +3866,8 @@ export class BackupImportStream extends Writable { const logId = `fromAdhocCall(${callId.slice(-2)})`; strictAssert(callTimestamp, `${logId}: must have a valid timestamp`); - strictAssert(recipientIdLong, 'AdHocCall must have a recipientIdLong'); + strictAssert(recipientId != null, 'AdHocCall must have a recipientIdLong'); - const recipientId = recipientIdLong.toNumber(); const callLink = this.#recipientIdToCallLink.get(recipientId); if (!callLink) { @@ -3806,9 +3897,7 @@ export class BackupImportStream extends Writable { } } - async #fromNotificationProfile( - incomingProfile: Backups.INotificationProfile - ) { + async #fromNotificationProfile(incomingProfile: Backups.NotificationProfile) { const { id, name, @@ -3831,8 +3920,7 @@ export class BackupImportStream extends Writable { const allowedMemberConversationIds: ReadonlyArray | undefined = allowedMembers - ?.map(recipientIdLong => { - const recipientId = recipientIdLong.toNumber(); + ?.map(recipientId => { const attributes = this.#recipientIdToConvo.get(recipientId); if (!attributes) { return undefined; @@ -3854,7 +3942,7 @@ export class BackupImportStream extends Writable { scheduleEnabled: Boolean(scheduleEnabled), scheduleStartTime: dropNull(scheduleStartTime), scheduleEndTime: dropNull(scheduleEndTime), - scheduleDaysEnabled: fromDayOfWeekArray(scheduleDaysEnabled), + scheduleDaysEnabled: parseScheduleDaysEnabled(scheduleDaysEnabled), deletedAtTimestampMs: undefined, storageNeedsSync: false, storageID: undefined, @@ -3867,7 +3955,7 @@ export class BackupImportStream extends Writable { #chatFolderPositionCursor = 0; - async #fromChatFolder(proto: Backups.IChatFolder): Promise { + async #fromChatFolder(proto: Backups.ChatFolder): Promise { if (proto.id == null || proto.id.length === 0) { log.warn('Dropping chat folder; it was missing an id'); return; @@ -3903,12 +3991,12 @@ export class BackupImportStream extends Writable { includeAllIndividualChats: proto.includeAllIndividualChats ?? false, includeAllGroupChats: proto.includeAllGroupChats ?? false, includedConversationIds: includedRecipientIds.map(recipientId => { - const convo = this.#recipientIdToConvo.get(recipientId.toNumber()); + const convo = this.#recipientIdToConvo.get(recipientId); strictAssert(convo != null, 'Missing chat folder included recipient'); return convo.id; }), excludedConversationIds: excludedRecipientIds.map(recipientId => { - const convo = this.#recipientIdToConvo.get(recipientId.toNumber()); + const convo = this.#recipientIdToConvo.get(recipientId); strictAssert(convo != null, 'Missing chat folder included recipient'); return convo.id; }), @@ -3929,7 +4017,7 @@ export class BackupImportStream extends Writable { async #fromCustomChatColors( customChatColors: - | ReadonlyArray + | ReadonlyArray | undefined | null ): Promise { @@ -3944,17 +4032,19 @@ export class BackupImportStream extends Writable { order, }; - for (const color of customChatColors) { + for (const customChatColor of customChatColors) { + const { color } = customChatColor; + const uuid = generateUuid(); let value: CustomColorType; order.push(uuid); - if (color.solid) { + if (color?.solid) { value = { start: rgbIntToDesktopHSL(color.solid), }; - } else if (color.gradient) { + } else if (color?.gradient) { strictAssert(color.gradient != null, 'Either solid or gradient'); strictAssert(color.gradient.colors != null, 'Missing gradient colors'); @@ -3985,7 +4075,7 @@ export class BackupImportStream extends Writable { } customColors.colors[uuid] = value; - this.#customColorById.set(color.id?.toNumber() || 0, { + this.#customColorById.set(toNumber(customChatColor.id) || 0, { id: uuid, value, }); @@ -3994,7 +4084,7 @@ export class BackupImportStream extends Writable { await itemStorage.put('customColors', customColors); } - #fromChatStyle(chatStyle: Backups.IChatStyle | null | undefined): Omit< + #fromChatStyle(chatStyle: Backups.ChatStyle | null | undefined): Omit< LocalChatStyle, 'customColorId' > & { @@ -4015,28 +4105,34 @@ export class BackupImportStream extends Writable { let wallpaperPreset: number | undefined; const dimWallpaperInDarkMode = dropNull(chatStyle.dimWallpaperInDarkMode); - if (chatStyle.wallpaperPhoto) { + if (chatStyle.wallpaper?.wallpaperPhoto) { wallpaperPhotoPointer = Backups.FilePointer.encode( - chatStyle.wallpaperPhoto - ).finish(); - } else if (chatStyle.wallpaperPreset != null) { - wallpaperPreset = chatStyle.wallpaperPreset; + chatStyle.wallpaper.wallpaperPhoto + ); + } else if ( + chatStyle.wallpaper?.wallpaperPreset != null && + isKnownProtoEnumMember( + Backups.ChatStyle.WallpaperPreset, + chatStyle.wallpaper.wallpaperPreset + ) + ) { + wallpaperPreset = chatStyle.wallpaper.wallpaperPreset; } let color: ConversationColorType | undefined; let customColorData: CustomColorDataType | undefined; let autoBubbleColor = false; - if (chatStyle.autoBubbleColor) { + if (chatStyle.bubbleColor?.autoBubbleColor) { autoBubbleColor = true; if (wallpaperPreset != null) { color = WALLPAPER_TO_BUBBLE_COLOR.get(wallpaperPreset); } else { color = undefined; } - } else if (chatStyle.bubbleColorPreset != null) { + } else if (chatStyle.bubbleColor?.bubbleColorPreset != null) { const { BubbleColorPreset } = Backups.ChatStyle; - switch (chatStyle.bubbleColorPreset) { + switch (chatStyle.bubbleColor.bubbleColorPreset) { case BubbleColorPreset.SOLID_CRIMSON: color = 'crimson'; break; @@ -4105,9 +4201,9 @@ export class BackupImportStream extends Writable { color = 'ultramarine'; break; } - } else if (chatStyle.customColorId != null) { + } else if (chatStyle.bubbleColor?.customColorId != null) { const entry = this.#customColorById.get( - chatStyle.customColorId.toNumber() + toNumber(chatStyle.bubbleColor.customColorId) ); if (entry) { @@ -4167,7 +4263,7 @@ function rgbIntToDesktopHSL(intValue: number): { } function fromGroupCallStateProto( - state: Backups.GroupCall.State | undefined | null + state?: Backups.GroupCall['state'] ): GroupCallStatus { const values = Backups.GroupCall.State; @@ -4204,7 +4300,7 @@ function fromGroupCallStateProto( } function fromIndividualCallDirectionProto( - direction: Backups.IndividualCall.Direction | undefined | null + direction?: Backups.IndividualCall['direction'] ): CallDirection { const values = Backups.IndividualCall.Direction; @@ -4222,7 +4318,7 @@ function fromIndividualCallDirectionProto( } function fromIndividualCallTypeProto( - type: Backups.IndividualCall.Type | undefined | null + type?: Backups.IndividualCall['type'] ): CallType { const values = Backups.IndividualCall.Type; @@ -4240,7 +4336,7 @@ function fromIndividualCallTypeProto( } function fromIndividualCallStateProto( - status: Backups.IndividualCall.State | undefined | null + status?: Backups.IndividualCall['state'] ): DirectCallStatus { const values = Backups.IndividualCall.State; @@ -4264,26 +4360,34 @@ function fromIndividualCallStateProto( } function fromAdHocCallStateProto( - status: Backups.AdHocCall.State | undefined | null + status?: Backups.AdHocCall['state'] ): AdhocCallStatus { const values = Backups.AdHocCall.State; - if (status == null) { + if ( + status == null || + !isKnownProtoEnumMember(values, status) || + status === values.UNKNOWN_STATE + ) { return AdhocCallStatus.Unknown; } if (status === values.GENERIC) { return AdhocCallStatus.Generic; } - return AdhocCallStatus.Unknown; + throw missingCaseError(status); } function fromCallLinkRestrictionsProto( - restrictions: Backups.CallLink.Restrictions | undefined | null + restrictions?: Backups.CallLink['restrictions'] ): CallLinkRestrictions { const values = Backups.CallLink.Restrictions; - if (restrictions == null) { + if ( + restrictions == null || + !isKnownProtoEnumMember(values, restrictions) || + restrictions === values.UNKNOWN + ) { return CallLinkRestrictions.Unknown; } if (restrictions === values.NONE) { @@ -4293,12 +4397,24 @@ function fromCallLinkRestrictionsProto( return CallLinkRestrictions.AdminApproval; } - return CallLinkRestrictions.Unknown; + throw missingCaseError(restrictions); +} + +function parseAvatarColorFromPrimary( + color?: Backups.Contact['avatarColor'] +): Backups.AvatarColor | undefined { + if (isKnownProtoEnumMember(Backups.AvatarColor, color)) { + return color; + } + return undefined; } function fromAvatarColor( - color: Backups.AvatarColor | null | undefined + color?: Backups.Contact['avatarColor'] ): string | undefined { + if (!isKnownProtoEnumMember(Backups.AvatarColor, color)) { + return undefined; + } switch (color) { case Backups.AvatarColor.A100: return 'A100'; @@ -4333,7 +4449,7 @@ function fromAvatarColor( } function toThemeSetting( - theme: Backups.AccountData.AppTheme | undefined | null + theme?: Backups.AccountData.AccountSettings['appTheme'] ): ThemeType { const ENUM = Backups.AccountData.AppTheme; @@ -4346,3 +4462,137 @@ function toThemeSetting( return 'system'; } + +function toBackupLevel( + input?: Backups.AccountData.AccountSettings['backupTier'] +): BackupLevel | null { + if (input == null) { + return null; + } + const number = toNumber(input); + if (number === BackupLevel.Free) { + return BackupLevel.Free; + } + if (number === BackupLevel.Paid) { + return BackupLevel.Paid; + } + return null; +} + +function parseAutoDownloadOption( + input?: Backups.AccountData.AutoDownloadSettings['images'] // or others +): Exclude< + Backups.AccountData.AutoDownloadSettings.AutoDownloadOption, + Backups.AccountData.AutoDownloadSettings.AutoDownloadOption.UNKNOWN +> { + const { AutoDownloadOption } = Backups.AccountData.AutoDownloadSettings; + + if (input == null || !isKnownProtoEnumMember(AutoDownloadOption, input)) { + return AutoDownloadOption.NEVER; + } + + if ( + input === AutoDownloadOption.WIFI || + input === AutoDownloadOption.WIFI_AND_CELLULAR || + input === AutoDownloadOption.NEVER + ) { + return input; + } + + if (input === AutoDownloadOption.UNKNOWN) { + return AutoDownloadOption.NEVER; + } + + throw missingCaseError(input); +} + +function parseGroupAccessRequired( + input?: Backups.Group.AccessControl['attributes'] +): Backups.Group.AccessControl.AccessRequired { + const { AccessRequired } = Backups.Group.AccessControl; + + if (input == null || !isKnownProtoEnumMember(AccessRequired, input)) { + return AccessRequired.UNKNOWN; + } + + if ( + input === AccessRequired.ANY || + input === AccessRequired.ADMINISTRATOR || + input === AccessRequired.MEMBER || + input === AccessRequired.UNSATISFIABLE || + input === AccessRequired.UNKNOWN + ) { + return input; + } + + throw missingCaseError(input); +} + +function parseGroupV2AccessLevel( + input?: Backups.GroupMembershipAccessLevelChangeUpdate['accessLevel'] +): Backups.GroupV2AccessLevel { + if ( + input == null || + !isKnownProtoEnumMember(Backups.GroupV2AccessLevel, input) + ) { + return Backups.GroupV2AccessLevel.UNKNOWN; + } + + if ( + input === Backups.GroupV2AccessLevel.ANY || + input === Backups.GroupV2AccessLevel.ADMINISTRATOR || + input === Backups.GroupV2AccessLevel.MEMBER || + input === Backups.GroupV2AccessLevel.UNSATISFIABLE || + input === Backups.GroupV2AccessLevel.UNKNOWN + ) { + return input; + } + + throw missingCaseError(input); +} + +function parseGroupMemberRole( + input?: Backups.Group.Member['role'] +): Backups.Group.Member.Role { + const { Role } = Backups.Group.Member; + + if (input == null || !isKnownProtoEnumMember(Role, input)) { + return Role.UNKNOWN; + } + + if ( + input === Role.ADMINISTRATOR || + input === Role.DEFAULT || + input === Role.UNKNOWN + ) { + return input; + } + + throw missingCaseError(input); +} + +function parseScheduleDaysEnabled( + input?: Backups.NotificationProfile['scheduleDaysEnabled'] +): NotificationProfileType['scheduleDaysEnabled'] { + return fromDayOfWeekArray( + input?.filter(item => { + return isKnownProtoEnumMember( + Backups.NotificationProfile.DayOfWeek, + item + ); + }) + ); +} + +function parseIdentityState( + input?: Backups.Contact['identityState'] +): Backups.Contact.IdentityState { + if ( + input == null || + !isKnownProtoEnumMember(Backups.Contact.IdentityState, input) + ) { + return Backups.Contact.IdentityState.DEFAULT; + } + + return input; +} diff --git a/ts/services/backups/util/filePointers.preload.ts b/ts/services/backups/util/filePointers.preload.ts index 3068917b23..fcf7e2a222 100644 --- a/ts/services/backups/util/filePointers.preload.ts +++ b/ts/services/backups/util/filePointers.preload.ts @@ -45,6 +45,8 @@ import { } from '../../../types/Crypto.std.js'; import type { BackupExportOptions, BackupImportOptions } from '../types.std.js'; import { isTestOrMockEnvironment } from '../../../environment.std.js'; +import { toNumber } from '../../../util/toNumber.std.js'; +import { isKnownProtoEnumMember } from '../../../util/isKnownProtoEnumMember.std.js'; export function convertFilePointerToAttachment( filePointer: Backups.FilePointer, @@ -95,8 +97,7 @@ export function convertFilePointerToAttachment( const { key, localKey, - plaintextHash, - encryptedDigest, + integrityCheck, size, transitCdnKey, transitCdnNumber, @@ -113,11 +114,14 @@ export function convertFilePointerToAttachment( } let mediaName: string | undefined; - if (Bytes.isNotEmpty(plaintextHash) && Bytes.isNotEmpty(key)) { + if ( + Bytes.isNotEmpty(integrityCheck?.plaintextHash) && + Bytes.isNotEmpty(key) + ) { mediaName = getMediaName({ key, - plaintextHash, + plaintextHash: integrityCheck.plaintextHash, }) ?? undefined; } @@ -125,9 +129,12 @@ export function convertFilePointerToAttachment( if ( options.type === 'local-encrypted' && Bytes.isNotEmpty(localKey) && - Bytes.isNotEmpty(plaintextHash) + Bytes.isNotEmpty(integrityCheck?.plaintextHash) ) { - const localMediaName = getLocalBackupFileName({ plaintextHash, localKey }); + const localMediaName = getLocalBackupFileName({ + plaintextHash: integrityCheck.plaintextHash, + localKey, + }); localBackupPath = getAttachmentLocalBackupPathFromSnapshotDir( localMediaName, options.localBackupSnapshotDir @@ -137,8 +144,8 @@ export function convertFilePointerToAttachment( return { ...commonProps, key: Bytes.toBase64(key), - digest: Bytes.isNotEmpty(encryptedDigest) - ? Bytes.toBase64(encryptedDigest) + digest: Bytes.isNotEmpty(integrityCheck?.encryptedDigest) + ? Bytes.toBase64(integrityCheck.encryptedDigest) : undefined, size: size ?? 0, cdnKey: transitCdnKey ?? undefined, @@ -146,8 +153,8 @@ export function convertFilePointerToAttachment( uploadTimestamp: transitTierUploadTimestamp ? getTimestampFromLong(transitTierUploadTimestamp) : undefined, - plaintextHash: Bytes.isNotEmpty(plaintextHash) - ? Bytes.toHex(plaintextHash) + plaintextHash: Bytes.isNotEmpty(integrityCheck?.plaintextHash) + ? Bytes.toHex(integrityCheck.plaintextHash) : undefined, localBackupPath, // TODO: DESKTOP-8883 @@ -161,7 +168,7 @@ export function convertFilePointerToAttachment( } export function convertBackupMessageAttachmentToAttachment( - messageAttachment: Backups.IMessageAttachment, + messageAttachment: Backups.MessageAttachment, options: BackupImportOptions ): AttachmentType | null { const { clientUuid } = messageAttachment; @@ -174,7 +181,12 @@ export function convertBackupMessageAttachmentToAttachment( clientUuid: clientUuid ? bytesToUuid(clientUuid) : undefined, }; - switch (messageAttachment.flag) { + let { flag } = messageAttachment; + if (!isKnownProtoEnumMember(Backups.MessageAttachment.Flag, flag)) { + flag = Backups.MessageAttachment.Flag.NONE; + } + + switch (flag) { case Backups.MessageAttachment.Flag.VOICE_MESSAGE: result.flags = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE; break; @@ -185,12 +197,10 @@ export function convertBackupMessageAttachmentToAttachment( result.flags = SignalService.AttachmentPointer.Flags.GIF; break; case Backups.MessageAttachment.Flag.NONE: - case null: - case undefined: result.flags = undefined; break; default: - throw missingCaseError(messageAttachment.flag); + throw missingCaseError(flag); } return result; @@ -207,19 +217,22 @@ export async function getFilePointerForAttachment({ backupOptions: BackupExportOptions; messageReceivedAt: number; }): Promise<{ - filePointer: Backups.FilePointer; + filePointer: Backups.FilePointer.Params; backupJob?: CoreAttachmentBackupJobType | CoreAttachmentLocalBackupJobType; }> { const attachment = maybeFixupAttachment(rawAttachment); - const filePointer = new Backups.FilePointer({ + const filePointer: Backups.FilePointer.Params = { contentType: attachment.contentType, - fileName: attachment.fileName, - width: attachment.width, - height: attachment.height, - caption: attachment.caption, - blurHash: attachment.blurHash, - }); + fileName: attachment.fileName ?? null, + width: attachment.width ?? null, + height: attachment.height ?? null, + caption: attachment.caption ?? null, + blurHash: attachment.blurHash ?? null, + incrementalMac: null, + incrementalMacChunkSize: null, + locatorInfo: null, + }; // TODO: DESKTOP-9112 if (isTestOrMockEnvironment()) { @@ -325,7 +338,8 @@ export async function getFilePointerForAttachment({ ? { cdnKey: transitCdnKey, cdnNumber: transitCdnNumber, - uploadTimestamp: transitTierUploadTimestamp?.toNumber(), + uploadTimestamp: + toNumber(transitTierUploadTimestamp) ?? undefined, } : undefined, }, @@ -366,9 +380,7 @@ function getLocatorInfoForAttachment({ backupOptions: BackupExportOptions; isOnDisk: boolean; backupTierInfo: BackupCdnInfoType; -}): Backups.FilePointer.LocatorInfo { - const locatorInfo = new Backups.FilePointer.LocatorInfo(); - +}): Backups.FilePointer.LocatorInfo.Params { const isLocalBackup = backupOptions.type === 'local-encrypted' || backupOptions.type === 'plaintext-export'; @@ -386,38 +398,64 @@ function getLocatorInfoForAttachment({ !isDownloadableFromTransitTier && !hasRequiredInformationForRemoteBackup(attachment) ) { - return locatorInfo; - } - - locatorInfo.size = attachment.size; - - if (isValidAttachmentKey(attachment.key)) { - locatorInfo.key = Bytes.fromBase64(attachment.key); + return { + size: null, + key: null, + integrityCheck: null, + transitCdnKey: null, + transitCdnNumber: null, + transitTierUploadTimestamp: null, + localKey: null, + mediaTierCdnNumber: null, + }; } + let integrityCheck: Backups.FilePointer.LocatorInfo.Params['integrityCheck']; if (isValidPlaintextHash(attachment.plaintextHash)) { - locatorInfo.plaintextHash = Bytes.fromHex(attachment.plaintextHash); + integrityCheck = { + plaintextHash: Bytes.fromHex(attachment.plaintextHash), + }; } else if (isValidDigest(attachment.digest)) { - locatorInfo.encryptedDigest = Bytes.fromBase64(attachment.digest); + integrityCheck = { + encryptedDigest: Bytes.fromBase64(attachment.digest), + }; + } else { + integrityCheck = null; } - if (isDownloadableFromTransitTier) { - locatorInfo.transitCdnKey = attachment.cdnKey; - locatorInfo.transitCdnNumber = attachment.cdnNumber; - locatorInfo.transitTierUploadTimestamp = getSafeLongFromTimestamp( - attachment.uploadTimestamp - ); - } - - if (shouldBeLocallyBackedUp) { - locatorInfo.localKey = Bytes.fromBase64(attachment.localKey); - } + let mediaTierCdnNumber: Backups.FilePointer.LocatorInfo.Params['mediaTierCdnNumber']; if (backupTierInfo.isInBackupTier && backupTierInfo.cdnNumber != null) { - locatorInfo.mediaTierCdnNumber = backupTierInfo.cdnNumber; + mediaTierCdnNumber = backupTierInfo.cdnNumber; } else if (backupOptions.type === 'cross-client-integration-test') { - locatorInfo.mediaTierCdnNumber = attachment.backupCdnNumber; + mediaTierCdnNumber = attachment.backupCdnNumber ?? null; + } else { + mediaTierCdnNumber = null; } - return locatorInfo; + return { + size: attachment.size, + key: isValidAttachmentKey(attachment.key) + ? Bytes.fromBase64(attachment.key) + : null, + integrityCheck, + + ...(isDownloadableFromTransitTier + ? { + transitCdnKey: attachment.cdnKey, + transitCdnNumber: attachment.cdnNumber, + transitTierUploadTimestamp: getSafeLongFromTimestamp( + attachment.uploadTimestamp + ), + } + : { + transitCdnKey: null, + transitCdnNumber: null, + transitTierUploadTimestamp: null, + }), + localKey: shouldBeLocallyBackedUp + ? Bytes.fromBase64(attachment.localKey) + : null, + mediaTierCdnNumber, + }; } diff --git a/ts/services/backups/util/localBackup.node.ts b/ts/services/backups/util/localBackup.node.ts index 2f88ec0a77..49e83357ed 100644 --- a/ts/services/backups/util/localBackup.node.ts +++ b/ts/services/backups/util/localBackup.node.ts @@ -13,6 +13,7 @@ import * as Errors from '../../../types/errors.std.js'; import { Signal } from '../../../protobuf/index.std.js'; import { DelimitedStream } from '../../../util/DelimitedStream.node.js'; import { strictAssert } from '../../../util/assert.std.js'; +import { encodeDelimited } from '../../../util/encodeDelimited.std.js'; import { decryptAesCtr, encryptAesCtr } from '../../../Crypto.node.js'; import type { LocalBackupMetadataVerificationType } from '../../../types/backups.node.js'; import { @@ -220,12 +221,12 @@ export async function writeLocalBackupMetadata({ const encryptedId = encryptAesCtr(metadataKey, backupId, iv); const metadataSerialized = Signal.backup.local.Metadata.encode({ - backupId: new Signal.backup.local.Metadata.EncryptedBackupId({ + backupId: { iv, encryptedId, - }), + }, version: LOCAL_BACKUP_VERSION, - }).finish(); + }); const metadataPath = join(snapshotDir, 'metadata'); await writeFile(metadataPath, metadataSerialized); @@ -279,11 +280,13 @@ export async function writeLocalBackupFilesList({ function* generateFrames() { for (const mediaName of mediaNames) { - const data = Signal.backup.local.FilesFrame.encodeDelimited({ - mediaName, - }).finish(); + const data = Signal.backup.local.FilesFrame.encode({ + item: { + mediaName, + }, + }); - yield data; + yield* encodeDelimited(data); files.push(mediaName); } @@ -308,8 +311,8 @@ export async function readLocalBackupFilesList( write(data, _enc, callback) { try { const file = Signal.backup.local.FilesFrame.decode(data); - if (file.mediaName) { - mediaNames.push(file.mediaName); + if (file.item?.mediaName) { + mediaNames.push(file.item.mediaName); } else { log.warn( 'ParseFilesListTransform: Active file had empty mediaName, ignoring' diff --git a/ts/services/calling.preload.ts b/ts/services/calling.preload.ts index c7c6b2de04..5174ed7eb0 100644 --- a/ts/services/calling.preload.ts +++ b/ts/services/calling.preload.ts @@ -34,6 +34,7 @@ import { HangupType, IceCandidateMessage, OfferMessage, + OfferType, OpaqueMessage, RingCancelReason, RingRTC, @@ -43,7 +44,6 @@ import { } from '@signalapp/ringrtc'; import * as muteStateChange from '@signalapp/mute-state-change'; import lodash from 'lodash'; -import Long from 'long'; import type { CallLinkAuthCredentialPresentation } from '@signalapp/libsignal-client/zkgroup.js'; import { CallLinkSecretParams, @@ -70,6 +70,7 @@ import type { ConversationType } from '../state/ducks/conversations.preload.js'; import { getConversationCallMode } from '../state/ducks/conversations.preload.js'; import { isMe } from '../util/whatTypeOfConversation.dom.js'; import { getAbsoluteTempPath } from '../util/migrations.preload.js'; +import { isKnownProtoEnumMember } from '../util/isKnownProtoEnumMember.std.js'; import type { AvailableIODevicesType, IceServerType, @@ -364,6 +365,48 @@ function maybeShowCallQualitySurvey( }, CALL_QUALITY_SURVEY_DELAY); } +function protoOfferTypeToCalling( + offer: Proto.CallMessage.Offer['type'] +): OfferType { + if (!isKnownProtoEnumMember(Proto.CallMessage.Offer.Type, offer)) { + return OfferType.AudioCall; + } + + const { Type } = Proto.CallMessage.Offer; + switch (offer) { + case Type.OFFER_AUDIO_CALL: + return OfferType.AudioCall; + case Type.OFFER_VIDEO_CALL: + return OfferType.VideoCall; + default: + throw missingCaseError(offer); + } +} + +function protoHangupTypeToCalling( + hangup: Proto.CallMessage.Hangup['type'] +): HangupType { + if (!isKnownProtoEnumMember(Proto.CallMessage.Hangup.Type, hangup)) { + return HangupType.Normal; + } + + const { Type } = Proto.CallMessage.Hangup; + switch (hangup) { + case Type.HANGUP_NORMAL: + return HangupType.Normal; + case Type.HANGUP_ACCEPTED: + return HangupType.Accepted; + case Type.HANGUP_DECLINED: + return HangupType.Declined; + case Type.HANGUP_BUSY: + return HangupType.Busy; + case Type.HANGUP_NEED_PERMISSION: + return HangupType.NeedPermission; + default: + throw missingCaseError(hangup); + } +} + function protoToCallingMessage({ offer, answer, @@ -372,7 +415,7 @@ function protoToCallingMessage({ hangup, destinationDeviceId, opaque, -}: Proto.ICallMessage): CallingMessage { +}: Proto.CallMessage): CallingMessage { const newIceCandidates: Array = []; if (iceUpdate) { iceUpdate.forEach(candidate => { @@ -389,7 +432,7 @@ function protoToCallingMessage({ offer && offer.id && offer.opaque ? new OfferMessage( offer.id, - dropNull(offer.type) as number, + protoOfferTypeToCalling(offer.type), offer.opaque ) : undefined, @@ -403,7 +446,7 @@ function protoToCallingMessage({ hangup && hangup.id ? new HangupMessage( hangup.id, - dropNull(hangup.type) as number, + protoHangupTypeToCalling(hangup.type), hangup.deviceId || 0 ) : undefined, @@ -2996,7 +3039,7 @@ export class CallingClass { async handleCallingMessage( envelope: ProcessedEnvelope, - callingMessage: Proto.ICallMessage + callingMessage: Proto.CallMessage ): Promise { const logId = `CallingClass.handleCallingMessage(${envelope.timestamp})`; log.info(logId); @@ -3232,7 +3275,7 @@ export class CallingClass { callingMessage.opaque.data = data; const proto = callingMessageToProto(callingMessage, urgency); - const protoBytes = Proto.CallMessage.encode(proto).finish(); + const protoBytes = Proto.CallMessage.encode(proto); const protoBase64 = Bytes.toBase64(protoBytes); await conversationJobQueue.add({ @@ -3398,7 +3441,7 @@ export class CallingClass { try { const proto = callingMessageToProto(message, urgency); - const protoBytes = Proto.CallMessage.encode(proto).finish(); + const protoBytes = Proto.CallMessage.encode(proto); const protoBase64 = Bytes.toBase64(protoBytes); const job = await conversationJobQueue.add({ @@ -3530,7 +3573,7 @@ export class CallingClass { const callEndedReason = this.#convertRingRtcCallRejectReason(callRejectReason); - const callId = Long.fromValue(callIdValue).toString(); + const callId = callIdValue.toString(); const peerId = getPeerIdFromConversation(conversation.attributes); // This is extra defensive, just in case RingRTC passes us a bad value. (It probably diff --git a/ts/services/storage.preload.ts b/ts/services/storage.preload.ts index 9145f7b379..b1d3a83945 100644 --- a/ts/services/storage.preload.ts +++ b/ts/services/storage.preload.ts @@ -3,7 +3,6 @@ import lodash from 'lodash'; import pMap from 'p-map'; -import Long from 'long'; import { DataReader, DataWriter } from '../sql/Client.preload.js'; import * as Bytes from '../Bytes.std.js'; @@ -104,11 +103,13 @@ import { isCurrentAllChatFolder } from '../types/CurrentChatFolders.std.js'; import type { NotificationProfileType } from '../types/NotificationProfile.std.js'; import { itemStorage } from '../textsecure/Storage.preload.js'; +import { toNumber } from '../util/toNumber.std.js'; + const { debounce, isNumber, chunk } = lodash; const log = createLogger('storage'); -type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier; +type IManifestRecordIdentifier = Proto.ManifestRecord.Identifier.Params; const { getItemById } = DataReader; @@ -154,10 +155,8 @@ const conflictBackOff = new BackOff([ function encryptRecord( storageID: string | undefined, recordIkm: Uint8Array | undefined, - storageRecord: Proto.IStorageRecord -): Proto.StorageItem { - const storageItem = new Proto.StorageItem(); - + storageRecord: Proto.StorageRecord.Params +): Proto.StorageItem.Params { const storageKeyBuffer = storageID ? Bytes.fromBase64(storageID) : generateStorageID(); @@ -174,14 +173,14 @@ function encryptRecord( }); const encryptedRecord = encryptProfile( - Proto.StorageRecord.encode(storageRecord).finish(), + Proto.StorageRecord.encode(storageRecord), storageItemKey ); - storageItem.key = storageKeyBuffer; - storageItem.value = encryptedRecord; - - return storageItem; + return { + key: storageKeyBuffer, + value: encryptedRecord, + }; } function generateStorageID(): Uint8Array { @@ -191,14 +190,14 @@ function generateStorageID(): Uint8Array { type GeneratedManifestType = { postUploadUpdateFunctions: Array<() => unknown>; recordIkm: Uint8Array | undefined; - recordsByID: Map; + recordsByID: Map; insertKeys: Set; deleteKeys: Set; }; async function generateManifest( version: number, - previousManifest?: Proto.IManifestRecord, + previousManifest?: Proto.ManifestRecord, isNewManifest = false ): Promise { log.info(`upload(${version}): generating manifest new=${isNewManifest}`); @@ -214,7 +213,7 @@ async function generateManifest( const postUploadUpdateFunctions: Array<() => unknown> = []; const insertKeys = new Set(); const deleteKeys = new Set(); - const recordsByID = new Map(); + const recordsByID = new Map(); function processStorageRecord({ conversation, @@ -229,7 +228,7 @@ async function generateManifest( currentStorageVersion?: number | null; identifierType: Proto.ManifestRecord.Identifier.Type; storageNeedsSync: boolean; - storageRecord: Proto.IStorageRecord; + storageRecord: Proto.StorageRecord.Params; }) { const currentRedactedID = currentStorageID ? redactStorageID(currentStorageID, currentStorageVersion) @@ -275,7 +274,7 @@ async function generateManifest( const conversation = conversations[i]; let identifierType; - let storageRecord; + let storageRecord: Proto.StorageRecord.Params | undefined; if (isSignalConversation(conversation.attributes)) { continue; @@ -283,11 +282,14 @@ async function generateManifest( const conversationType = typeofConversation(conversation.attributes); if (conversationType === ConversationTypes.Me) { - storageRecord = new Proto.StorageRecord(); - // eslint-disable-next-line no-await-in-loop - storageRecord.account = await toAccountRecord(conversation, { - notificationProfileSyncDisabled, - }); + storageRecord = { + record: { + // eslint-disable-next-line no-await-in-loop + account: await toAccountRecord(conversation, { + notificationProfileSyncDisabled, + }), + }, + }; identifierType = ITEM_TYPE.ACCOUNT; } else if (conversationType === ConversationTypes.Direct) { // Contacts must have UUID @@ -332,17 +334,26 @@ async function generateManifest( continue; } - storageRecord = new Proto.StorageRecord(); - // eslint-disable-next-line no-await-in-loop - storageRecord.contact = await toContactRecord(conversation); + storageRecord = { + record: { + // eslint-disable-next-line no-await-in-loop + contact: await toContactRecord(conversation), + }, + }; identifierType = ITEM_TYPE.CONTACT; } else if (conversationType === ConversationTypes.GroupV2) { - storageRecord = new Proto.StorageRecord(); - storageRecord.groupV2 = toGroupV2Record(conversation); + storageRecord = { + record: { + groupV2: toGroupV2Record(conversation), + }, + }; identifierType = ITEM_TYPE.GROUPV2; } else if (conversationType === ConversationTypes.GroupV1) { - storageRecord = new Proto.StorageRecord(); - storageRecord.groupV1 = toGroupV1Record(conversation); + storageRecord = { + record: { + groupV1: toGroupV1Record(conversation), + }, + }; identifierType = ITEM_TYPE.GROUPV1; } else { log.warn( @@ -393,10 +404,13 @@ async function generateManifest( ); for (const storyDistributionList of storyDistributionLists) { - const storageRecord = new Proto.StorageRecord(); - storageRecord.storyDistributionList = toStoryDistributionListRecord( - storyDistributionList - ); + const storageRecord: Proto.StorageRecord.Params = { + record: { + storyDistributionList: toStoryDistributionListRecord( + storyDistributionList + ), + }, + }; if ( storyDistributionList.deletedAtTimestamp != null && @@ -461,9 +475,11 @@ async function generateManifest( ); } for (const notificationProfile of notificationProfilesToUpload) { - const storageRecord = new Proto.StorageRecord(); - storageRecord.notificationProfile = - toNotificationProfileRecord(notificationProfile); + const storageRecord: Proto.StorageRecord.Params = { + record: { + notificationProfile: toNotificationProfileRecord(notificationProfile), + }, + }; if ( notificationProfile.deletedAtTimestampMs != null && @@ -520,8 +536,11 @@ async function generateManifest( let newlyUninstalledPacks = 0; uninstalledStickerPacks.forEach(stickerPack => { - const storageRecord = new Proto.StorageRecord(); - storageRecord.stickerPack = toStickerPackRecord(stickerPack); + const storageRecord: Proto.StorageRecord.Params = { + record: { + stickerPack: toStickerPackRecord(stickerPack), + }, + }; uninstalledStickerPackIds.add(stickerPack.id); @@ -561,8 +580,11 @@ async function generateManifest( return; } - const storageRecord = new Proto.StorageRecord(); - storageRecord.stickerPack = toStickerPackRecord(stickerPack); + const storageRecord: Proto.StorageRecord.Params = { + record: { + stickerPack: toStickerPackRecord(stickerPack), + }, + }; const { isNewItem, storageID } = processStorageRecord({ currentStorageID: stickerPack.storageID, @@ -605,8 +627,11 @@ async function generateManifest( continue; } - const storageRecord = new Proto.StorageRecord(); - storageRecord.callLink = toCallLinkRecord(callLinkDbRecord); + const storageRecord: Proto.StorageRecord.Params = { + record: { + callLink: toCallLinkRecord(callLinkDbRecord), + }, + }; const callLink = callLinkFromRecord(callLinkDbRecord); callLinkRoomIds.add(callLink.roomId); @@ -649,8 +674,11 @@ async function generateManifest( ); defunctCallLinks.forEach(defunctCallLink => { - const storageRecord = new Proto.StorageRecord(); - storageRecord.callLink = toDefunctOrPendingCallLinkRecord(defunctCallLink); + const storageRecord: Proto.StorageRecord.Params = { + record: { + callLink: toDefunctOrPendingCallLinkRecord(defunctCallLink), + }, + }; callLinkRoomIds.add(defunctCallLink.roomId); @@ -682,8 +710,11 @@ async function generateManifest( ); pendingCallLinks.forEach(pendingCallLink => { - const storageRecord = new Proto.StorageRecord(); - storageRecord.callLink = toDefunctOrPendingCallLinkRecord(pendingCallLink); + const storageRecord: Proto.StorageRecord.Params = { + record: { + callLink: toDefunctOrPendingCallLinkRecord(pendingCallLink), + }, + }; const roomId = getRoomIdFromRootKeyString(pendingCallLink.rootKey); if (callLinkRoomIds.has(roomId)) { @@ -721,9 +752,11 @@ async function generateManifest( currentStorageVersion: chatFolder.storageVersion ?? undefined, identifierType: ITEM_TYPE.CHAT_FOLDER, storageNeedsSync: chatFolder.storageNeedsSync, - storageRecord: new Proto.StorageRecord({ - chatFolder: toChatFolderRecord(chatFolder), - }), + storageRecord: { + record: { + chatFolder: toChatFolderRecord(chatFolder), + }, + }, }); if (isNewItem) { @@ -953,14 +986,14 @@ async function generateManifest( } type EncryptManifestOptionsType = { - recordsByID: Map; + recordsByID: Map; recordIkm: Uint8Array | undefined; insertKeys: Set; }; type EncryptedManifestType = { - newItems: Set; - storageManifest: Proto.IStorageManifest; + newItems: Set; + storageManifest: Proto.StorageManifest.Params; }; async function encryptManifest( @@ -968,13 +1001,13 @@ async function encryptManifest( { recordsByID, recordIkm, insertKeys }: EncryptManifestOptionsType ): Promise { const manifestRecordKeys: Set = new Set(); - const newItems: Set = new Set(); + const newItems: Set = new Set(); for (const [storageID, { itemType, storageRecord }] of recordsByID) { - const identifier = new Proto.ManifestRecord.Identifier({ + const identifier: Proto.ManifestRecord.Identifier.Params = { type: itemType, raw: Bytes.fromBase64(storageID), - }); + }; manifestRecordKeys.add(identifier); @@ -999,13 +1032,12 @@ async function encryptManifest( } } - const manifestRecord = new Proto.ManifestRecord(); - manifestRecord.version = Long.fromNumber(version); - manifestRecord.sourceDevice = itemStorage.user.getDeviceId() ?? 0; - manifestRecord.identifiers = Array.from(manifestRecordKeys); - if (recordIkm != null) { - manifestRecord.recordIkm = recordIkm; - } + const manifestRecord: Proto.ManifestRecord.Params = { + version: BigInt(version), + sourceDevice: itemStorage.user.getDeviceId() ?? 0, + identifiers: Array.from(manifestRecordKeys), + recordIkm: recordIkm ?? null, + }; const storageKeyBase64 = itemStorage.get('storageKey'); if (!storageKeyBase64) { @@ -1014,20 +1046,19 @@ async function encryptManifest( const storageKey = Bytes.fromBase64(storageKeyBase64); const storageManifestKey = deriveStorageManifestKey( storageKey, - Long.fromNumber(version) + BigInt(version) ); const encryptedManifest = encryptProfile( - Proto.ManifestRecord.encode(manifestRecord).finish(), + Proto.ManifestRecord.encode(manifestRecord), storageManifestKey ); - const storageManifest = new Proto.StorageManifest(); - storageManifest.version = manifestRecord.version; - storageManifest.value = encryptedManifest; - return { newItems, - storageManifest, + storageManifest: { + version: manifestRecord.version, + value: encryptedManifest, + }, }; } @@ -1048,19 +1079,18 @@ async function uploadManifest( `deleting=${deleteKeys.size}` ); - const writeOperation = new Proto.WriteOperation(); - writeOperation.manifest = storageManifest; - writeOperation.insertItem = Array.from(newItems); - writeOperation.deleteKey = Array.from(deleteKeys).map(storageID => - Bytes.fromBase64(storageID) - ); + const writeOperation = Proto.WriteOperation.encode({ + manifest: storageManifest, + insertItem: Array.from(newItems), + deleteKey: Array.from(deleteKeys).map(storageID => + Bytes.fromBase64(storageID) + ), + clearAll: false, + }); - await modifyStorageRecords( - Proto.WriteOperation.encode(writeOperation).finish(), - { - credentials, - } - ); + await modifyStorageRecords(writeOperation, { + credentials, + }); log.info( `upload(${version}): upload complete, updating ` + @@ -1141,7 +1171,7 @@ async function createNewManifest() { } async function decryptManifest( - encryptedManifest: Proto.IStorageManifest + encryptedManifest: Proto.StorageManifest ): Promise { const { version, value } = encryptedManifest; @@ -1201,10 +1231,16 @@ async function fetchManifest( return undefined; } +type GeneratedItemType = { + itemType: number; + storageID: string; + storageRecord: Proto.StorageRecord.Params; +}; + type MergeableItemType = { itemType: number; storageID: string; - storageRecord: Proto.IStorageRecord; + storageRecord: Proto.StorageRecord; }; type MergedRecordType = UnknownRecord & { @@ -1235,68 +1271,86 @@ async function mergeRecord( // Note: when updating this switch, update the validRecordTypes set upfile if (itemType === ITEM_TYPE.UNKNOWN) { log.warn('mergeRecord: Unknown item type', redactedStorageID); - } else if (itemType === ITEM_TYPE.CONTACT && storageRecord.contact) { + } else if ( + itemType === ITEM_TYPE.CONTACT && + storageRecord.record?.contact + ) { mergeResult = await mergeContactRecord( storageID, storageVersion, - storageRecord.contact + storageRecord.record.contact ); - } else if (itemType === ITEM_TYPE.GROUPV1 && storageRecord.groupV1) { + } else if ( + itemType === ITEM_TYPE.GROUPV1 && + storageRecord.record?.groupV1 + ) { mergeResult = await mergeGroupV1Record( storageID, storageVersion, - storageRecord.groupV1 + storageRecord.record.groupV1 ); - } else if (itemType === ITEM_TYPE.GROUPV2 && storageRecord.groupV2) { + } else if ( + itemType === ITEM_TYPE.GROUPV2 && + storageRecord.record?.groupV2 + ) { mergeResult = await mergeGroupV2Record( storageID, storageVersion, - storageRecord.groupV2 + storageRecord.record.groupV2 ); - } else if (itemType === ITEM_TYPE.ACCOUNT && storageRecord.account) { + } else if ( + itemType === ITEM_TYPE.ACCOUNT && + storageRecord.record?.account + ) { mergeResult = await mergeAccountRecord( storageID, storageVersion, - storageRecord.account + storageRecord.record.account ); } else if ( itemType === ITEM_TYPE.STORY_DISTRIBUTION_LIST && - storageRecord.storyDistributionList + storageRecord.record?.storyDistributionList ) { mergeResult = await mergeStoryDistributionListRecord( storageID, storageVersion, - storageRecord.storyDistributionList + storageRecord.record.storyDistributionList ); } else if ( itemType === ITEM_TYPE.STICKER_PACK && - storageRecord.stickerPack + storageRecord.record?.stickerPack ) { mergeResult = await mergeStickerPackRecord( storageID, storageVersion, - storageRecord.stickerPack + storageRecord.record.stickerPack ); - } else if (itemType === ITEM_TYPE.CALL_LINK && storageRecord.callLink) { + } else if ( + itemType === ITEM_TYPE.CALL_LINK && + storageRecord.record?.callLink + ) { mergeResult = await mergeCallLinkRecord( storageID, storageVersion, - storageRecord.callLink + storageRecord.record.callLink ); - } else if (itemType === ITEM_TYPE.CHAT_FOLDER && storageRecord.chatFolder) { + } else if ( + itemType === ITEM_TYPE.CHAT_FOLDER && + storageRecord.record?.chatFolder + ) { mergeResult = await mergeChatFolderRecord( storageID, storageVersion, - storageRecord.chatFolder + storageRecord.record.chatFolder ); } else if ( itemType === ITEM_TYPE.NOTIFICATION_PROFILE && - storageRecord.notificationProfile + storageRecord.record?.notificationProfile ) { mergeResult = await mergeNotificationProfileRecord( storageID, storageVersion, - storageRecord.notificationProfile + storageRecord.record.notificationProfile ); } else { isUnsupported = true; @@ -1394,7 +1448,7 @@ async function getNonConversationRecords(): Promise { const remoteKeysTypeMap = new Map(); @@ -1805,18 +1859,17 @@ async function fetchRemoteRecords( batches, async ( batch: ReadonlyArray - ): Promise> => { - const readOperation = new Proto.ReadOperation(); - readOperation.readKey = batch.map(Bytes.fromBase64); - + ): Promise> => { const storageItemsBuffer = await getStorageRecords( - Proto.ReadOperation.encode(readOperation).finish(), + Proto.ReadOperation.encode({ + readKey: batch.map(Bytes.fromBase64), + }), { credentials, } ); - return Proto.StorageItems.decode(storageItemsBuffer).items ?? []; + return Proto.StorageItems.decode(storageItemsBuffer).items; }, { concurrency: 5 } ) @@ -1827,7 +1880,7 @@ async function fetchRemoteRecords( const decryptedItems = await pMap( storageItems, async ( - storageRecordWrapper: Proto.IStorageItem + storageRecordWrapper: Proto.StorageItem ): Promise => { const { key, value: storageItemCiphertext } = storageRecordWrapper; @@ -1909,9 +1962,12 @@ async function processRemoteRecords( // Drop all GV1 records for which we have GV2 record in the same manifest const masterKeys = new Map(); for (const { itemType, storageID, storageRecord } of decryptedItems) { - if (itemType === ITEM_TYPE.GROUPV2 && storageRecord.groupV2?.masterKey) { + if ( + itemType === ITEM_TYPE.GROUPV2 && + storageRecord.record?.groupV2?.masterKey + ) { masterKeys.set( - Bytes.toBase64(storageRecord.groupV2.masterKey), + Bytes.toBase64(storageRecord.record.groupV2.masterKey), storageID ); } @@ -1941,7 +1997,7 @@ async function processRemoteRecords( } accountItem = item; - const record = accountItem?.storageRecord.account; + const record = accountItem?.storageRecord.record?.account; if (!record) { log.warn( @@ -1953,11 +2009,13 @@ async function processRemoteRecords( return false; } - if (itemType !== ITEM_TYPE.GROUPV1 || !storageRecord.groupV1?.id) { + if (itemType !== ITEM_TYPE.GROUPV1 || !storageRecord.record?.groupV1?.id) { return true; } - const masterKey = deriveMasterKeyFromGroupV1(storageRecord.groupV1.id); + const masterKey = deriveMasterKeyFromGroupV1( + storageRecord.record?.groupV1.id + ); const gv2StorageID = masterKeys.get(Bytes.toBase64(masterKey)); if (!gv2StorageID) { return true; @@ -1980,7 +2038,7 @@ async function processRemoteRecords( const splitPNIContacts = new Array(); prunedStorageItems = prunedStorageItems.filter(item => { const { itemType, storageRecord } = item; - const { contact } = storageRecord; + const contact = storageRecord.record?.contact; if (itemType !== ITEM_TYPE.CONTACT || !contact) { return true; } @@ -2224,7 +2282,7 @@ async function sync({ } strictAssert(manifest.version != null, 'Manifest without version'); - const version = manifest.version?.toNumber() ?? 0; + const version = toNumber(manifest.version) ?? 0; await window.waitForEmptyEventQueue(); @@ -2446,7 +2504,7 @@ export async function reprocessUnknownFields(): Promise { 'Inserted records must have storageRecord' ); - if (!item.storageRecord.$unknownFields?.length) { + if (!item.storageRecord.$unknown?.length) { return undefined; } @@ -2454,7 +2512,7 @@ export async function reprocessUnknownFields(): Promise { ...item, storageRecord: Proto.StorageRecord.decode( - Proto.StorageRecord.encode(item.storageRecord).finish() + Proto.StorageRecord.encode(item.storageRecord) ), }; }), diff --git a/ts/services/storageRecordOps.preload.ts b/ts/services/storageRecordOps.preload.ts index ecac74acbb..59c02bd0f2 100644 --- a/ts/services/storageRecordOps.preload.ts +++ b/ts/services/storageRecordOps.preload.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import lodash, { omit, partition, without } from 'lodash'; -import Long from 'long'; import { ServiceId } from '@signalapp/libsignal-client'; import { uuidToBytes, bytesToUuid } from '../util/uuidToBytes.std.js'; @@ -130,6 +129,9 @@ import { import { itemStorage } from '../textsecure/Storage.preload.js'; import { onHasStoriesDisabledChange } from '../textsecure/WebAPI.preload.js'; import { keyTransparency } from './keyTransparency.preload.js'; +import { toNumber } from '../util/toNumber.std.js'; +import { MAX_VALUE } from '../util/long.std.js'; +import { isKnownProtoEnumMember } from '../util/isKnownProtoEnumMember.std.js'; const { isEqual } = lodash; @@ -138,10 +140,10 @@ const log = createLogger('storageRecordOps'); const MY_STORY_BYTES = uuidToBytes(MY_STORY_ID); type RecordClass = - | Proto.IAccountRecord - | Proto.IContactRecord - | Proto.IGroupV1Record - | Proto.IGroupV2Record; + | Proto.AccountRecord.Params + | Proto.ContactRecord.Params + | Proto.GroupV1Record.Params + | Proto.GroupV2Record.Params; export type MergeResultType = Readonly<{ shouldDrop?: boolean; @@ -168,7 +170,7 @@ function toRecordVerified(verified: number): Proto.ContactRecord.IdentityState { } function fromRecordVerified( - verified: Proto.ContactRecord.IdentityState + verified: Proto.ContactRecord['identityState'] ): number { const VERIFIED_ENUM = signalProtocolStore.VerifiedStatus; const STATE_ENUM = Proto.ContactRecord.IdentityState; @@ -184,7 +186,11 @@ function fromRecordVerified( } function fromAvatarColor( - color: Proto.AvatarColor | null | undefined + color: ( + | Proto.ContactRecord + | Proto.GroupV2Record + | Proto.AccountRecord + )['avatarColor'] ): string | undefined { switch (color) { case Proto.AvatarColor.A100: @@ -215,16 +221,21 @@ function fromAvatarColor( case null: return undefined; default: - throw missingCaseError(color); + // Default value for proto + return 'A100'; } } function applyAvatarColor( conversation: ConversationModel, - protoColor: Proto.AvatarColor | null | undefined + protoColor: ( + | Proto.ContactRecord + | Proto.GroupV2Record + | Proto.AccountRecord + )['avatarColor'] ): void { conversation.set({ - colorFromPrimary: dropNull(protoColor), + colorFromPrimary: dropNull(protoColor as number), color: fromAvatarColor(protoColor) ?? conversation.get('color'), }); } @@ -235,12 +246,10 @@ function addUnknownFieldsToConversation( conversation: ConversationModel, details: Array ): void { - if (record.$unknownFields) { + if (record.$unknown) { details.push('adding unknown fields'); conversation.set({ - storageUnknownFields: Bytes.toBase64( - Bytes.concatenate(record.$unknownFields) - ), + storageUnknownFields: Bytes.toBase64(Bytes.concatenate(record.$unknown)), }); } else if (conversation.get('storageUnknownFields')) { // If the record doesn't have unknown fields attached but we have them @@ -250,19 +259,18 @@ function addUnknownFieldsToConversation( } } -function applyConversationUnknownFieldsToRecord( - record: RecordClass, +function conversationUnknownFieldsToRecord( conversation: ConversationModel -): void { +): Array | null { const storageUnknownFields = conversation.get('storageUnknownFields'); - if (storageUnknownFields) { - log.info( - 'storageService.applyUnknownFields: Applying unknown fields for', - conversation.idForLogging() - ); - // eslint-disable-next-line no-param-reassign - record.$unknownFields = [Bytes.fromBase64(storageUnknownFields)]; + if (storageUnknownFields == null) { + return null; } + log.info( + 'storageService.applyUnknownFields: Applying unknown fields for', + conversation.idForLogging() + ); + return [Bytes.fromBase64(storageUnknownFields)]; } // Other records save a UInt8Array to the database @@ -276,10 +284,10 @@ function toStorageUnknownFields( return Bytes.concatenate(unknownFields); } function fromStorageUnknownFields( - storageUnknownFields: Uint8Array | null -): ReadonlyArray | undefined { - if (!storageUnknownFields) { - return undefined; + storageUnknownFields: Uint8Array | null | undefined +): Array | null { + if (storageUnknownFields == null) { + return null; } return [storageUnknownFields]; @@ -287,106 +295,70 @@ function fromStorageUnknownFields( export async function toContactRecord( conversation: ConversationModel -): Promise { - const contactRecord = new Proto.ContactRecord(); +): Promise { const aci = conversation.getAci(); - if (aci) { - if (isProtoBinaryEncodingEnabled()) { - contactRecord.aciBinary = toAciObject(aci).getRawUuidBytes(); - } else { - contactRecord.aci = aci; - } - } - const e164 = conversation.get('e164'); - if (e164) { - contactRecord.e164 = e164; - } const username = conversation.get('username'); const ourID = window.ConversationController.getOurConversationId(); - if (username && canHaveUsername(conversation.attributes, ourID)) { - contactRecord.username = username; - } const pni = conversation.getPni(); - if (pni) { - if (isProtoBinaryEncodingEnabled()) { - contactRecord.pniBinary = toPniObject(pni).getRawUuidBytes(); - } else { - contactRecord.pni = toUntaggedPni(pni); - } - } - contactRecord.pniSignatureVerified = - conversation.get('pniSignatureVerified') ?? false; - const profileKey = conversation.get('profileKey'); - if (profileKey) { - contactRecord.profileKey = Bytes.fromBase64(String(profileKey)); - } + const profileKey = conversation.get('profileKey'); const serviceId = aci ?? pni; - const identityKey = serviceId - ? await signalProtocolStore.loadIdentityKey(serviceId) - : undefined; - if (identityKey) { - contactRecord.identityKey = identityKey; - } const verified = conversation.get('verified'); - if (verified) { - contactRecord.identityState = toRecordVerified(Number(verified)); - } - const profileName = conversation.get('profileName'); - if (profileName) { - contactRecord.givenName = profileName; - } - const profileFamilyName = conversation.get('profileFamilyName'); - if (profileFamilyName) { - contactRecord.familyName = profileFamilyName; - } const nicknameGivenName = conversation.get('nicknameGivenName'); const nicknameFamilyName = conversation.get('nicknameFamilyName'); - if (nicknameGivenName || nicknameFamilyName) { - contactRecord.nickname = { - given: nicknameGivenName, - family: nicknameFamilyName, - }; - } - const note = conversation.get('note'); - if (note) { - contactRecord.note = note; - } - const systemGivenName = conversation.get('systemGivenName'); - if (systemGivenName) { - contactRecord.systemGivenName = systemGivenName; - } - const systemFamilyName = conversation.get('systemFamilyName'); - if (systemFamilyName) { - contactRecord.systemFamilyName = systemFamilyName; - } - const systemNickname = conversation.get('systemNickname'); - if (systemNickname) { - contactRecord.systemNickname = systemNickname; - } - contactRecord.blocked = conversation.isBlocked(); - contactRecord.hidden = conversation.get('removalStage') !== undefined; - contactRecord.whitelisted = Boolean(conversation.get('profileSharing')); - contactRecord.archived = Boolean(conversation.get('isArchived')); - contactRecord.markedUnread = Boolean(conversation.get('markedUnread')); - contactRecord.mutedUntilTimestamp = getSafeLongFromTimestamp( - conversation.get('muteExpiresAt'), - Long.MAX_VALUE - ); - if (conversation.get('hideStory') !== undefined) { - contactRecord.hideStory = Boolean(conversation.get('hideStory')); - } - contactRecord.unregisteredAtTimestamp = getSafeLongFromTimestamp( - conversation.get('firstUnregisteredAt') - ); - const avatarColor = conversation.get('colorFromPrimary'); - if (avatarColor != null) { - contactRecord.avatarColor = avatarColor; - } + const hideStory = conversation.get('hideStory'); - applyConversationUnknownFieldsToRecord(contactRecord, conversation); - - return contactRecord; + return { + e164: conversation.get('e164') ?? null, + aciBinary: + isProtoBinaryEncodingEnabled() && aci + ? toAciObject(aci).getRawUuidBytes() + : null, + aci: !isProtoBinaryEncodingEnabled() && aci ? aci : null, + pniBinary: + isProtoBinaryEncodingEnabled() && pni + ? toPniObject(pni).getRawUuidBytes() + : null, + pni: !isProtoBinaryEncodingEnabled() && pni ? toUntaggedPni(pni) : null, + username: + username && canHaveUsername(conversation.attributes, ourID) + ? username + : null, + identityKey: serviceId + ? ((await signalProtocolStore.loadIdentityKey(serviceId)) ?? null) + : null, + identityState: verified ? toRecordVerified(Number(verified)) : null, + pniSignatureVerified: conversation.get('pniSignatureVerified') ?? false, + profileKey: profileKey ? Bytes.fromBase64(String(profileKey)) : null, + givenName: conversation.get('profileName') ?? null, + familyName: conversation.get('profileFamilyName') ?? null, + nickname: + nicknameGivenName || nicknameFamilyName + ? { + given: nicknameGivenName ?? null, + family: nicknameFamilyName ?? null, + } + : null, + note: conversation.get('note') ?? null, + systemGivenName: conversation.get('systemGivenName') ?? null, + systemFamilyName: conversation.get('systemFamilyName') ?? null, + systemNickname: conversation.get('systemNickname') ?? null, + blocked: conversation.isBlocked(), + hidden: conversation.get('removalStage') !== undefined, + whitelisted: Boolean(conversation.get('profileSharing')), + archived: Boolean(conversation.get('isArchived')), + markedUnread: Boolean(conversation.get('markedUnread')), + mutedUntilTimestamp: getSafeLongFromTimestamp( + conversation.get('muteExpiresAt'), + MAX_VALUE + ), + avatarColor: conversation.get('colorFromPrimary') ?? null, + hideStory: hideStory != null ? Boolean(hideStory) : null, + unregisteredAtTimestamp: getSafeLongFromTimestamp( + conversation.get('firstUnregisteredAt') + ), + $unknown: conversationUnknownFieldsToRecord(conversation), + }; } export function toAccountRecord( @@ -394,80 +366,35 @@ export function toAccountRecord( { notificationProfileSyncDisabled, }: { notificationProfileSyncDisabled: boolean } -): Proto.AccountRecord { - const accountRecord = new Proto.AccountRecord(); - - if (conversation.get('profileKey')) { - accountRecord.profileKey = Bytes.fromBase64( - String(conversation.get('profileKey')) - ); - } - if (conversation.get('profileName')) { - accountRecord.givenName = conversation.get('profileName') || ''; - } - if (conversation.get('profileFamilyName')) { - accountRecord.familyName = conversation.get('profileFamilyName') || ''; - } - const avatarUrl = itemStorage.get('avatarUrl'); - if (avatarUrl !== undefined) { - accountRecord.avatarUrlPath = avatarUrl; - } - const username = conversation.get('username'); - if (username !== undefined) { - accountRecord.username = username; - } - accountRecord.noteToSelfArchived = Boolean(conversation.get('isArchived')); - accountRecord.noteToSelfMarkedUnread = Boolean( - conversation.get('markedUnread') - ); - accountRecord.readReceipts = getReadReceiptSetting(); - accountRecord.sealedSenderIndicators = getSealedSenderIndicatorSetting(); - accountRecord.typingIndicators = getTypingIndicatorSetting(); - accountRecord.linkPreviews = getLinkPreviewSetting(); - - const preferContactAvatars = itemStorage.get('preferContactAvatars'); - if (preferContactAvatars !== undefined) { - accountRecord.preferContactAvatars = Boolean(preferContactAvatars); - } - - const rawPreferredReactionEmoji = itemStorage.get('preferredReactionEmoji'); - if (preferredReactionEmoji.canBeSynced(rawPreferredReactionEmoji)) { - accountRecord.preferredReactionEmoji = rawPreferredReactionEmoji; - } - - const universalExpireTimer = getUniversalExpireTimer(); - if (universalExpireTimer) { - accountRecord.universalExpireTimer = Number(universalExpireTimer); - } - +): Proto.AccountRecord.Params { const PHONE_NUMBER_SHARING_MODE_ENUM = Proto.AccountRecord.PhoneNumberSharingMode; - const phoneNumberSharingMode = parsePhoneNumberSharingMode( + const localPhoneNumberSharingMode = parsePhoneNumberSharingMode( itemStorage.get('phoneNumberSharingMode') ); - switch (phoneNumberSharingMode) { + let phoneNumberSharingMode: Proto.AccountRecord.PhoneNumberSharingMode; + switch (localPhoneNumberSharingMode) { case PhoneNumberSharingMode.Everybody: - accountRecord.phoneNumberSharingMode = - PHONE_NUMBER_SHARING_MODE_ENUM.EVERYBODY; + phoneNumberSharingMode = PHONE_NUMBER_SHARING_MODE_ENUM.EVERYBODY; break; case PhoneNumberSharingMode.ContactsOnly: case PhoneNumberSharingMode.Nobody: - accountRecord.phoneNumberSharingMode = - PHONE_NUMBER_SHARING_MODE_ENUM.NOBODY; + phoneNumberSharingMode = PHONE_NUMBER_SHARING_MODE_ENUM.NOBODY; break; default: - throw missingCaseError(phoneNumberSharingMode); + throw missingCaseError(localPhoneNumberSharingMode); } const phoneNumberDiscoverability = parsePhoneNumberDiscoverability( itemStorage.get('phoneNumberDiscoverability') ); + let unlistedPhoneNumber: boolean; switch (phoneNumberDiscoverability) { case PhoneNumberDiscoverability.Discoverable: - accountRecord.unlistedPhoneNumber = false; + unlistedPhoneNumber = false; break; case PhoneNumberDiscoverability.NotDiscoverable: - accountRecord.unlistedPhoneNumber = true; + unlistedPhoneNumber = true; break; default: throw missingCaseError(phoneNumberDiscoverability); @@ -475,383 +402,363 @@ export function toAccountRecord( const pinnedConversations = itemStorage .get('pinnedConversationIds', new Array()) - .map(id => { + .map((id): Proto.AccountRecord.PinnedConversation.Params | undefined => { const pinnedConversation = window.ConversationController.get(id); - if (pinnedConversation) { - const pinnedConversationRecord = - new Proto.AccountRecord.PinnedConversation(); - - if (pinnedConversation.get('type') === 'private') { - const serviceId = pinnedConversation.getServiceId(); - pinnedConversationRecord.identifier = 'contact'; - pinnedConversationRecord.contact = { - ...(isProtoBinaryEncodingEnabled() - ? { - serviceIdBinary: - serviceId == null - ? null - : toServiceIdObject(serviceId).getServiceIdBinary(), - } - : { - serviceId, - }), - e164: pinnedConversation.get('e164'), - }; - } else if (isGroupV1(pinnedConversation.attributes)) { - pinnedConversationRecord.identifier = 'legacyGroupId'; - const groupId = pinnedConversation.get('groupId'); - if (!groupId) { - throw new Error( - 'toAccountRecord: trying to pin a v1 Group without groupId' - ); - } - pinnedConversationRecord.legacyGroupId = Bytes.fromBinary(groupId); - } else if (isGroupV2(pinnedConversation.attributes)) { - pinnedConversationRecord.identifier = 'groupMasterKey'; - const masterKey = pinnedConversation.get('masterKey'); - if (!masterKey) { - throw new Error( - 'toAccountRecord: trying to pin a v2 Group without masterKey' - ); - } - pinnedConversationRecord.groupMasterKey = Bytes.fromBase64(masterKey); - } - - return pinnedConversationRecord; + if (!pinnedConversation) { + return undefined; } + if (pinnedConversation.get('type') === 'private') { + const serviceId = pinnedConversation.getServiceId(); + return { + identifier: { + contact: { + ...(isProtoBinaryEncodingEnabled() + ? { + serviceIdBinary: + serviceId == null + ? null + : toServiceIdObject(serviceId).getServiceIdBinary(), + serviceId: null, + } + : { + serviceId: serviceId ?? null, + serviceIdBinary: null, + }), + e164: pinnedConversation.get('e164') ?? null, + }, + }, + }; + } + if (isGroupV1(pinnedConversation.attributes)) { + const groupId = pinnedConversation.get('groupId'); + if (!groupId) { + throw new Error( + 'toAccountRecord: trying to pin a v1 Group without groupId' + ); + } + return { + identifier: { + legacyGroupId: Bytes.fromBinary(groupId), + }, + }; + } + if (isGroupV2(pinnedConversation.attributes)) { + const masterKey = pinnedConversation.get('masterKey'); + if (!masterKey) { + throw new Error( + 'toAccountRecord: trying to pin a v2 Group without masterKey' + ); + } + return { + identifier: { + groupMasterKey: Bytes.fromBase64(masterKey), + }, + }; + } return undefined; }) - .filter( - ( - pinnedConversationClass - ): pinnedConversationClass is Proto.AccountRecord.PinnedConversation => - pinnedConversationClass !== undefined - ); - - accountRecord.pinnedConversations = pinnedConversations; - - const subscriberId = itemStorage.get('subscriberId'); - if (Bytes.isNotEmpty(subscriberId)) { - accountRecord.donorSubscriberId = subscriberId; - } - const subscriberCurrencyCode = itemStorage.get('subscriberCurrencyCode'); - if (typeof subscriberCurrencyCode === 'string') { - accountRecord.donorSubscriberCurrencyCode = subscriberCurrencyCode; - } - const donorSubscriptionManuallyCanceled = itemStorage.get( - 'donorSubscriptionManuallyCancelled' - ); - if (typeof donorSubscriptionManuallyCanceled === 'boolean') { - accountRecord.donorSubscriptionManuallyCancelled = - donorSubscriptionManuallyCanceled; - } - - accountRecord.backupSubscriberData = generateBackupsSubscriberData(); - const backupTier = itemStorage.get('backupTier'); - if (backupTier) { - accountRecord.backupTier = Long.fromNumber(backupTier); - } - - const displayBadgesOnProfile = itemStorage.get('displayBadgesOnProfile'); - if (displayBadgesOnProfile !== undefined) { - accountRecord.displayBadgesOnProfile = displayBadgesOnProfile; - } - const keepMutedChatsArchived = itemStorage.get('keepMutedChatsArchived'); - if (keepMutedChatsArchived !== undefined) { - accountRecord.keepMutedChatsArchived = keepMutedChatsArchived; - } - - const hasSetMyStoriesPrivacy = itemStorage.get('hasSetMyStoriesPrivacy'); - if (hasSetMyStoriesPrivacy !== undefined) { - accountRecord.hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy; - } - - const hasViewedOnboardingStory = itemStorage.get('hasViewedOnboardingStory'); - if (hasViewedOnboardingStory !== undefined) { - accountRecord.hasViewedOnboardingStory = hasViewedOnboardingStory; - } - - const hasCompletedUsernameOnboarding = itemStorage.get( - 'hasCompletedUsernameOnboarding' - ); - if (hasCompletedUsernameOnboarding !== undefined) { - accountRecord.hasCompletedUsernameOnboarding = - hasCompletedUsernameOnboarding; - } - - const hasSeenGroupStoryEducationSheet = itemStorage.get( - 'hasSeenGroupStoryEducationSheet' - ); - if (hasSeenGroupStoryEducationSheet !== undefined) { - accountRecord.hasSeenGroupStoryEducationSheet = - hasSeenGroupStoryEducationSheet; - } - - const hasSeenAdminDeleteEducationDialog = itemStorage.get( - 'hasSeenAdminDeleteEducationDialog' - ); - if (hasSeenAdminDeleteEducationDialog != null) { - accountRecord.hasSeenAdminDeleteEducationDialog = - hasSeenAdminDeleteEducationDialog; - } - - const hasKeyTransparencyDisabled = itemStorage.get( - 'hasKeyTransparencyDisabled' - ); - accountRecord.automaticKeyVerificationDisabled = - hasKeyTransparencyDisabled === true; - - const hasStoriesDisabled = itemStorage.get('hasStoriesDisabled'); - accountRecord.storiesDisabled = hasStoriesDisabled === true; - - const storyViewReceiptsEnabled = itemStorage.get('storyViewReceiptsEnabled'); - if (storyViewReceiptsEnabled !== undefined) { - accountRecord.storyViewReceiptsEnabled = storyViewReceiptsEnabled - ? Proto.OptionalBool.ENABLED - : Proto.OptionalBool.DISABLED; - } else { - accountRecord.storyViewReceiptsEnabled = Proto.OptionalBool.UNSET; - } + .filter(isNotNil); // Username link + let usernameLink: Proto.AccountRecord.UsernameLink.Params | null = null; { const color = itemStorage.get('usernameLinkColor'); const linkData = itemStorage.get('usernameLink'); if (linkData?.entropy.length && linkData?.serverId.length) { - accountRecord.usernameLink = { - color, + usernameLink = { + color: color ?? null, entropy: linkData.entropy, serverId: linkData.serverId, }; } } - const avatarColor = conversation.get('colorFromPrimary'); - if (avatarColor != null) { - accountRecord.avatarColor = avatarColor; - } - - accountRecord.notificationProfileSyncDisabled = - notificationProfileSyncDisabled; - const override = notificationProfileSyncDisabled ? itemStorage.get('notificationProfileOverrideFromPrimary') : itemStorage.get('notificationProfileOverride'); + let notificationProfileManualOverride: Proto.AccountRecord.NotificationProfileManualOverride.Params | null = + null; if (override?.disabledAtMs && override?.disabledAtMs > 0) { - const overrideProto = - new Proto.AccountRecord.NotificationProfileManualOverride(); - - overrideProto.disabledAtTimestampMs = Long.fromNumber( - override.disabledAtMs - ); - - accountRecord.notificationProfileManualOverride = overrideProto; + notificationProfileManualOverride = { + override: { + disabledAtTimestampMs: BigInt(override.disabledAtMs), + }, + }; } else if (override?.enabled) { const { profileId, endsAtMs } = override.enabled; - const overrideProto = - new Proto.AccountRecord.NotificationProfileManualOverride(); - overrideProto.enabled = - new Proto.AccountRecord.NotificationProfileManualOverride.ManuallyEnabled(); - - overrideProto.enabled.id = Bytes.fromHex(profileId); - if (endsAtMs && endsAtMs > 0) { - overrideProto.enabled.endAtTimestampMs = Long.fromNumber(endsAtMs); - } - - accountRecord.notificationProfileManualOverride = overrideProto; + notificationProfileManualOverride = { + override: { + enabled: { + id: Bytes.fromHex(profileId), + endAtTimestampMs: endsAtMs && endsAtMs > 0 ? BigInt(endsAtMs) : null, + }, + }, + }; } - applyConversationUnknownFieldsToRecord(accountRecord, conversation); + const profileKey = conversation.get('profileKey'); + const storyViewReceiptsEnabled = itemStorage.get('storyViewReceiptsEnabled'); + const backupTier = itemStorage.get('backupTier'); - return accountRecord; + const subscriberId = itemStorage.get('subscriberId'); + const subscriberCurrencyCode = itemStorage.get('subscriberCurrencyCode'); + const donorSubscriptionManuallyCanceled = itemStorage.get( + 'donorSubscriptionManuallyCancelled' + ); + + const preferContactAvatars = itemStorage.get('preferContactAvatars'); + const rawPreferredReactionEmoji = itemStorage.get('preferredReactionEmoji'); + const universalExpireTimer = getUniversalExpireTimer(); + + let storyViewReceiptsEnabledValue: Proto.AccountRecord.Params['storyViewReceiptsEnabled']; + + if (storyViewReceiptsEnabled !== undefined) { + if (storyViewReceiptsEnabled) { + storyViewReceiptsEnabledValue = Proto.OptionalBool.ENABLED; + } else { + storyViewReceiptsEnabledValue = Proto.OptionalBool.DISABLED; + } + } else { + storyViewReceiptsEnabledValue = Proto.OptionalBool.UNSET; + } + + return { + profileKey: profileKey ? Bytes.fromBase64(String(profileKey)) : null, + givenName: conversation.get('profileName') ?? null, + familyName: conversation.get('profileFamilyName') ?? null, + avatarUrlPath: itemStorage.get('avatarUrl') ?? null, + username: conversation.get('username') ?? null, + noteToSelfArchived: Boolean(conversation.get('isArchived')), + noteToSelfMarkedUnread: Boolean(conversation.get('markedUnread')), + readReceipts: getReadReceiptSetting(), + sealedSenderIndicators: getSealedSenderIndicatorSetting(), + typingIndicators: getTypingIndicatorSetting(), + linkPreviews: getLinkPreviewSetting(), + + preferContactAvatars: + preferContactAvatars != null ? Boolean(preferContactAvatars) : null, + preferredReactionEmoji: preferredReactionEmoji.canBeSynced( + rawPreferredReactionEmoji + ) + ? rawPreferredReactionEmoji + : null, + + universalExpireTimer: universalExpireTimer + ? Number(universalExpireTimer) + : null, + + phoneNumberSharingMode, + unlistedPhoneNumber, + pinnedConversations, + + donorSubscriberId: Bytes.isNotEmpty(subscriberId) ? subscriberId : null, + donorSubscriberCurrencyCode: + typeof subscriberCurrencyCode === 'string' + ? subscriberCurrencyCode + : null, + donorSubscriptionManuallyCancelled: + typeof donorSubscriptionManuallyCanceled === 'boolean' + ? donorSubscriptionManuallyCanceled + : null, + + // TODO: DESKTOP-9870 + hasBackup: null, + backupSubscriberData: generateBackupsSubscriberData(), + backupTier: backupTier != null ? BigInt(backupTier) : null, + + displayBadgesOnProfile: itemStorage.get('displayBadgesOnProfile') ?? null, + keepMutedChatsArchived: itemStorage.get('keepMutedChatsArchived') ?? null, + + hasSetMyStoriesPrivacy: itemStorage.get('hasSetMyStoriesPrivacy') ?? null, + hasViewedOnboardingStory: + itemStorage.get('hasViewedOnboardingStory') ?? null, + + hasCompletedUsernameOnboarding: + itemStorage.get('hasCompletedUsernameOnboarding') ?? null, + + hasSeenGroupStoryEducationSheet: + itemStorage.get('hasSeenGroupStoryEducationSheet') ?? null, + + hasSeenAdminDeleteEducationDialog: + itemStorage.get('hasSeenAdminDeleteEducationDialog') ?? null, + + avatarColor: conversation.get('colorFromPrimary') ?? null, + automaticKeyVerificationDisabled: + itemStorage.get('hasKeyTransparencyDisabled') === true, + storiesDisabled: itemStorage.get('hasStoriesDisabled') === true, + + storyViewReceiptsEnabled: storyViewReceiptsEnabledValue, + usernameLink, + notificationProfileManualOverride, + notificationProfileSyncDisabled, + + $unknown: conversationUnknownFieldsToRecord(conversation), + }; } export function toGroupV1Record( conversation: ConversationModel -): Proto.GroupV1Record { - const groupV1Record = new Proto.GroupV1Record(); - - groupV1Record.id = Bytes.fromBinary(String(conversation.get('groupId'))); - - applyConversationUnknownFieldsToRecord(groupV1Record, conversation); - - return groupV1Record; +): Proto.GroupV1Record.Params { + return { + id: Bytes.fromBinary(String(conversation.get('groupId'))), + $unknown: conversationUnknownFieldsToRecord(conversation), + }; } export function toGroupV2Record( conversation: ConversationModel -): Proto.GroupV2Record { - const groupV2Record = new Proto.GroupV2Record(); - - const masterKey = conversation.get('masterKey'); - if (masterKey !== undefined) { - groupV2Record.masterKey = Bytes.fromBase64(masterKey); - } - groupV2Record.blocked = conversation.isBlocked(); - groupV2Record.whitelisted = Boolean(conversation.get('profileSharing')); - groupV2Record.archived = Boolean(conversation.get('isArchived')); - groupV2Record.markedUnread = Boolean(conversation.get('markedUnread')); - groupV2Record.mutedUntilTimestamp = getSafeLongFromTimestamp( - conversation.get('muteExpiresAt'), - Long.MAX_VALUE - ); - groupV2Record.dontNotifyForMentionsIfMuted = Boolean( - conversation.get('dontNotifyForMentionsIfMuted') - ); - groupV2Record.hideStory = Boolean(conversation.get('hideStory')); - const storySendMode = conversation.get('storySendMode'); - if (storySendMode !== undefined) { - if (storySendMode === StorySendMode.IfActive) { - groupV2Record.storySendMode = Proto.GroupV2Record.StorySendMode.DEFAULT; - } else if (storySendMode === StorySendMode.Never) { - groupV2Record.storySendMode = Proto.GroupV2Record.StorySendMode.DISABLED; - } else if (storySendMode === StorySendMode.Always) { - groupV2Record.storySendMode = Proto.GroupV2Record.StorySendMode.ENABLED; - } else { - throw missingCaseError(storySendMode); - } +): Proto.GroupV2Record.Params { + const localStorySendMode = conversation.get('storySendMode'); + let storySendMode: Proto.GroupV2Record.StorySendMode; + if ( + localStorySendMode === StorySendMode.IfActive || + localStorySendMode == null + ) { + storySendMode = Proto.GroupV2Record.StorySendMode.DEFAULT; + } else if (localStorySendMode === StorySendMode.Never) { + storySendMode = Proto.GroupV2Record.StorySendMode.DISABLED; + } else if (localStorySendMode === StorySendMode.Always) { + storySendMode = Proto.GroupV2Record.StorySendMode.ENABLED; + } else { + throw missingCaseError(localStorySendMode); } const avatarColor = conversation.get('colorFromPrimary'); - if (avatarColor != null) { - groupV2Record.avatarColor = avatarColor; - } - applyConversationUnknownFieldsToRecord(groupV2Record, conversation); + const masterKey = conversation.get('masterKey'); - return groupV2Record; + return { + masterKey: masterKey != null ? Bytes.fromBase64(masterKey) : null, + blocked: conversation.isBlocked(), + whitelisted: Boolean(conversation.get('profileSharing')), + archived: Boolean(conversation.get('isArchived')), + markedUnread: Boolean(conversation.get('markedUnread')), + mutedUntilTimestamp: getSafeLongFromTimestamp( + conversation.get('muteExpiresAt'), + MAX_VALUE + ), + dontNotifyForMentionsIfMuted: Boolean( + conversation.get('dontNotifyForMentionsIfMuted') + ), + hideStory: Boolean(conversation.get('hideStory')), + avatarColor: avatarColor ?? null, + storySendMode, + + $unknown: conversationUnknownFieldsToRecord(conversation), + }; } export function toStoryDistributionListRecord( storyDistributionList: StoryDistributionWithMembersType -): Proto.StoryDistributionListRecord { - const storyDistributionListRecord = new Proto.StoryDistributionListRecord(); +): Proto.StoryDistributionListRecord.Params { + return { + identifier: uuidToBytes(storyDistributionList.id), + name: storyDistributionList.name, + deletedAtTimestamp: getSafeLongFromTimestamp( + storyDistributionList.deletedAtTimestamp + ), + allowsReplies: Boolean(storyDistributionList.allowsReplies), + isBlockList: Boolean(storyDistributionList.isBlockList), + recipientServiceIdsBinary: isProtoBinaryEncodingEnabled() + ? storyDistributionList.members.map(serviceId => { + return toServiceIdObject(serviceId).getServiceIdBinary(); + }) + : null, + recipientServiceIds: isProtoBinaryEncodingEnabled() + ? null + : storyDistributionList.members, - storyDistributionListRecord.identifier = uuidToBytes( - storyDistributionList.id - ); - storyDistributionListRecord.name = storyDistributionList.name; - storyDistributionListRecord.deletedAtTimestamp = getSafeLongFromTimestamp( - storyDistributionList.deletedAtTimestamp - ); - storyDistributionListRecord.allowsReplies = Boolean( - storyDistributionList.allowsReplies - ); - storyDistributionListRecord.isBlockList = Boolean( - storyDistributionList.isBlockList - ); - - if (isProtoBinaryEncodingEnabled()) { - storyDistributionListRecord.recipientServiceIdsBinary = - storyDistributionList.members.map(serviceId => { - return toServiceIdObject(serviceId).getServiceIdBinary(); - }); - } else { - storyDistributionListRecord.recipientServiceIds = - storyDistributionList.members; - } - - if (storyDistributionList.storageUnknownFields) { - storyDistributionListRecord.$unknownFields = fromStorageUnknownFields( + $unknown: fromStorageUnknownFields( storyDistributionList.storageUnknownFields - ); - } - - return storyDistributionListRecord; + ), + }; } export function toStickerPackRecord( stickerPack: StickerPackInfoType -): Proto.StickerPackRecord { - const stickerPackRecord = new Proto.StickerPackRecord(); - - stickerPackRecord.packId = Bytes.fromHex(stickerPack.id); +): Proto.StickerPackRecord.Params { + const $unknown = fromStorageUnknownFields(stickerPack.storageUnknownFields); if (stickerPack.uninstalledAt !== undefined) { - stickerPackRecord.deletedAtTimestamp = Long.fromNumber( - stickerPack.uninstalledAt - ); - } else { - stickerPackRecord.packKey = Bytes.fromBase64(stickerPack.key); - if (stickerPack.position) { - stickerPackRecord.position = stickerPack.position; - } + return { + packId: Bytes.fromHex(stickerPack.id), + packKey: null, + position: null, + deletedAtTimestamp: BigInt(stickerPack.uninstalledAt), + $unknown, + }; } - if (stickerPack.storageUnknownFields) { - stickerPackRecord.$unknownFields = fromStorageUnknownFields( - stickerPack.storageUnknownFields - ); - } - - return stickerPackRecord; + return { + packId: Bytes.fromHex(stickerPack.id), + packKey: Bytes.fromBase64(stickerPack.key), + position: stickerPack.position ?? null, + deletedAtTimestamp: null, + $unknown, + }; } // callLinkDbRecord exposes additional fields not available on CallLinkType export function toCallLinkRecord( callLinkDbRecord: CallLinkRecord -): Proto.CallLinkRecord { +): Proto.CallLinkRecord.Params { strictAssert(callLinkDbRecord.rootKey, 'toCallLinkRecord: no rootKey'); - const callLinkRecord = new Proto.CallLinkRecord(); + const $unknown = fromStorageUnknownFields( + callLinkDbRecord.storageUnknownFields + ); - callLinkRecord.rootKey = callLinkDbRecord.rootKey; if (callLinkDbRecord.deleted === 1) { // adminKey is intentionally omitted for deleted call links. - callLinkRecord.deletedAtTimestampMs = Long.fromNumber( - callLinkDbRecord.deletedAt || new Date().getTime() - ); - } else { - strictAssert( - callLinkDbRecord.adminKey, - 'toCallLinkRecord: no adminPasskey' - ); - callLinkRecord.adminPasskey = callLinkDbRecord.adminKey; + return { + rootKey: callLinkDbRecord.rootKey, + adminPasskey: null, + deletedAtTimestampMs: BigInt( + callLinkDbRecord.deletedAt || new Date().getTime() + ), + $unknown, + }; } - - if (callLinkDbRecord.storageUnknownFields) { - callLinkRecord.$unknownFields = fromStorageUnknownFields( - callLinkDbRecord.storageUnknownFields - ); - } - - return callLinkRecord; + strictAssert(callLinkDbRecord.adminKey, 'toCallLinkRecord: no adminPasskey'); + return { + rootKey: callLinkDbRecord.rootKey, + adminPasskey: callLinkDbRecord.adminKey, + deletedAtTimestampMs: null, + $unknown, + }; } export function toDefunctOrPendingCallLinkRecord( callLink: DefunctCallLinkType | PendingCallLinkType -): Proto.CallLinkRecord { +): Proto.CallLinkRecord.Params { const rootKey = toRootKeyBytes(callLink.rootKey); - const adminKey = callLink.adminKey + const adminPasskey = callLink.adminKey ? toAdminKeyBytes(callLink.adminKey) : null; strictAssert(rootKey, 'toDefunctOrPendingCallLinkRecord: no rootKey'); - strictAssert(adminKey, 'toDefunctOrPendingCallLinkRecord: no adminPasskey'); + strictAssert( + adminPasskey, + 'toDefunctOrPendingCallLinkRecord: no adminPasskey' + ); - const callLinkRecord = new Proto.CallLinkRecord(); - - callLinkRecord.rootKey = rootKey; - callLinkRecord.adminPasskey = adminKey; - - if (callLink.storageUnknownFields) { - callLinkRecord.$unknownFields = fromStorageUnknownFields( - callLink.storageUnknownFields - ); - } - - return callLinkRecord; + return { + rootKey, + adminPasskey, + deletedAtTimestampMs: null, + $unknown: fromStorageUnknownFields(callLink.storageUnknownFields), + }; } function toRecipient( conversationId: string, logPrefix: string -): Proto.Recipient | undefined { +): Proto.Recipient.Params | undefined { const conversation = window.ConversationController.get(conversationId); if (conversation == null) { @@ -871,13 +778,15 @@ function toRecipient( ); const serviceIdBinary = ServiceId.parseFromServiceIdString(serviceId).getServiceIdBinary(); - return new Proto.Recipient({ - contact: new Proto.Recipient.Contact({ - serviceId, - e164: conversation.get('e164'), - serviceIdBinary, - }), - }); + return { + identifier: { + contact: { + serviceId, + e164: conversation.get('e164') ?? null, + serviceIdBinary, + }, + }, + }; } if (isGroupV2(conversation.attributes)) { @@ -886,15 +795,21 @@ function toRecipient( masterKey, `${logId}: Missing masterKey on groupV2 conversation` ); - return new Proto.Recipient({ - groupMasterKey: Bytes.fromBase64(masterKey), - }); + return { + identifier: { + groupMasterKey: Bytes.fromBase64(masterKey), + }, + }; } if (isGroupV1(conversation.attributes)) { - return new Proto.Recipient({ - legacyGroupId: conversation.getGroupIdBuffer(), - }); + const legacyGroupId = conversation.getGroupIdBuffer(); + strictAssert(legacyGroupId, 'GroupV1 must have an id'); + return { + identifier: { + legacyGroupId, + }, + }; } throw new Error(`${logPrefix}: Unexpected conversation type for recipient`); @@ -903,7 +818,7 @@ function toRecipient( function toRecipients( conversationIds: ReadonlyArray, logPrefix: string -): Array { +): Array { return conversationIds .map(conversationId => { return toRecipient(conversationId, logPrefix); @@ -925,10 +840,10 @@ function toChatFolderRecordFolderType( export function toChatFolderRecord( chatFolder: ChatFolder -): Proto.ChatFolderRecord { +): Proto.ChatFolderRecord.Params { const logId = `toChatFolderRecord(${chatFolder.id})`; - const chatFolderRecord = new Proto.ChatFolderRecord({ + return { id: uuidToBytes(chatFolder.id), name: chatFolder.name, position: chatFolder.position, @@ -939,19 +854,14 @@ export function toChatFolderRecord( folderType: toChatFolderRecordFolderType(chatFolder.folderType), includedRecipients: toRecipients(chatFolder.includedConversationIds, logId), excludedRecipients: toRecipients(chatFolder.excludedConversationIds, logId), - deletedAtTimestampMs: Long.fromNumber(chatFolder.deletedAtTimestampMs), - }); - - if (chatFolder.storageUnknownFields != null) { - chatFolderRecord.$unknownFields = [chatFolder.storageUnknownFields]; - } - - return chatFolderRecord; + deletedAtTimestampMs: BigInt(chatFolder.deletedAtTimestampMs), + $unknown: fromStorageUnknownFields(chatFolder.storageUnknownFields), + }; } export function toNotificationProfileRecord( profile: NotificationProfileType -): Proto.NotificationProfile { +): Proto.NotificationProfile.Params { const { id, name, @@ -969,40 +879,29 @@ export function toNotificationProfileRecord( storageUnknownFields, } = profile; const logId = `toNotificationProfileRecord(${redactNotificationProfileId(id)})`; - const proto = new Proto.NotificationProfile(); - proto.id = Bytes.fromHex(id); - proto.name = name; - if (emoji) { - proto.emoji = emoji; - } - proto.color = color; - proto.createdAtMs = Long.fromNumber(createdAtMs); - if (deletedAtTimestampMs) { - proto.deletedAtTimestampMs = Long.fromNumber(deletedAtTimestampMs); - } - proto.allowAllCalls = allowAllCalls; - proto.allowAllMentions = allowAllMentions; - proto.scheduleEnabled = scheduleEnabled; - - if (scheduleStartTime) { - proto.scheduleStartTime = scheduleStartTime; - } - if (scheduleEndTime) { - proto.scheduleEndTime = scheduleEndTime; - } - proto.scheduleDaysEnabled = toDayOfWeekArray(scheduleDaysEnabled) ?? []; - - proto.allowedMembers = toRecipients(Array.from(allowedMembers), logId); - - if (storageUnknownFields) { - proto.$unknownFields = fromStorageUnknownFields(storageUnknownFields); - } - - return proto; + return { + id: Bytes.fromHex(id), + name, + emoji: emoji ?? null, + color, + createdAtMs: BigInt(createdAtMs), + deletedAtTimestampMs: + deletedAtTimestampMs == null ? null : BigInt(deletedAtTimestampMs), + allowAllCalls, + allowAllMentions, + scheduleEnabled, + scheduleStartTime: scheduleStartTime ?? null, + scheduleEndTime: scheduleEndTime ?? null, + scheduleDaysEnabled: toDayOfWeekArray(scheduleDaysEnabled) ?? [], + allowedMembers: toRecipients(Array.from(allowedMembers), logId), + $unknown: fromStorageUnknownFields(storageUnknownFields), + }; } -type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV2Record; +type MessageRequestCapableRecord = + | Proto.ContactRecord.Params + | Proto.GroupV2Record.Params; async function applyMessageRequestState( record: MessageRequestCapableRecord, @@ -1050,8 +949,8 @@ type RecordClassObject = { }; function areNicknamesEqual( - local: Proto.ContactRecord.IName | undefined | null, - remote: Proto.ContactRecord.IName | undefined | null + local: Proto.ContactRecord.Name.Params | undefined | null, + remote: Proto.ContactRecord.Name.Params | undefined | null ): boolean { return local?.given === remote?.given && local?.family === remote?.family; } @@ -1086,15 +985,16 @@ function logRecordChanges( } // If both types are Long we can use Long's equals to compare them - if (Long.isLong(localValue) || typeof localValue === 'number') { - if (!Long.isLong(remoteValue) && typeof remoteValue !== 'number') { + if (typeof localValue === 'bigint' || typeof localValue === 'number') { + if ( + !(typeof remoteValue === 'bigint') && + typeof remoteValue !== 'number' + ) { details.push(`key=${key}: type mismatch`); continue; } - const areEqual = Long.fromValue(localValue).equals( - Long.fromValue(remoteValue) - ); + const areEqual = BigInt(localValue) === BigInt(remoteValue); if (!areEqual) { details.push(`key=${key}: different integers`); } @@ -1121,10 +1021,8 @@ function logRecordChanges( continue; } - const isRemoteNullish = - !remoteValue || (Long.isLong(remoteValue) && remoteValue.isZero()); - const isLocalNullish = - !localValue || (Long.isLong(localValue) && localValue.isZero()); + const isRemoteNullish = !remoteValue; + const isLocalNullish = !localValue; // Sometimes we get `null` values from Protobuf and they should default to // false, empty string, or 0 for these records we do not count them as @@ -1151,7 +1049,7 @@ function logRecordChanges( export async function mergeGroupV1Record( storageID: string, storageVersion: number, - groupV1Record: Proto.IGroupV1Record + groupV1Record: Proto.GroupV1Record ): Promise { const redactedStorageID = redactExtendedStorageID({ storageID, @@ -1301,7 +1199,7 @@ function getGroupV2Conversation( export async function mergeGroupV2Record( storageID: string, storageVersion: number, - groupV2Record: Proto.IGroupV2Record + groupV2Record: Proto.GroupV2Record ): Promise { const redactedStorageID = redactExtendedStorageID({ storageID, @@ -1331,7 +1229,7 @@ export async function mergeGroupV2Record( ) { storySendMode = StorySendMode.Always; } else { - throw missingCaseError(recordStorySendMode); + throw new Error(`Unsupported story send mode: ${recordStorySendMode}`); } const details = logRecordChanges( @@ -1408,22 +1306,24 @@ export async function mergeGroupV2Record( export async function mergeContactRecord( storageID: string, storageVersion: number, - originalContactRecord: Proto.IContactRecord + originalContactRecord: Proto.ContactRecord ): Promise { const contactRecord = { ...originalContactRecord, - aci: fromAciUuidBytesOrString( - originalContactRecord.aciBinary, - originalContactRecord.aci, - 'ContactRecord.aci' - ), - pni: fromPniUuidBytesOrUntaggedString( - originalContactRecord.pniBinary, - originalContactRecord.pni, - 'ContactRecord.pni' - ), - }; + aci: + fromAciUuidBytesOrString( + originalContactRecord.aciBinary, + originalContactRecord.aci, + 'ContactRecord.aci' + ) ?? null, + pni: + fromPniUuidBytesOrUntaggedString( + originalContactRecord.pniBinary, + originalContactRecord.pni, + 'ContactRecord.pni' + ) ?? null, + } satisfies Proto.ContactRecord.Params; const e164 = dropNull(contactRecord.e164); const { aci } = contactRecord; @@ -1527,7 +1427,7 @@ export async function mergeContactRecord( }); // https://github.com/signalapp/Signal-Android/blob/fc3db538bcaa38dc149712a483d3032c9c1f3998/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt#L921-L936 - if (contactRecord.identityKey) { + if (contactRecord.identityKey.length) { const verified = await conversation.safeGetVerified(); let { identityState } = contactRecord; if (identityState == null) { @@ -1602,7 +1502,7 @@ export async function mergeContactRecord( if ( !contactRecord.unregisteredAtTimestamp || - contactRecord.unregisteredAtTimestamp.equals(0) + contactRecord.unregisteredAtTimestamp === 0n ) { conversation.setRegistered({ fromStorageService: true, shouldSave: false }); } else { @@ -1628,7 +1528,7 @@ export async function mergeContactRecord( export async function mergeAccountRecord( storageID: string, storageVersion: number, - accountRecord: Proto.IAccountRecord + accountRecord: Proto.AccountRecord ): Promise { const { linkPreviews, @@ -1801,7 +1701,12 @@ export async function mergeAccountRecord( ); const remotelyPinnedConversations = pinnedConversations - .map(({ contact, legacyGroupId, groupMasterKey }) => { + .map(({ identifier }) => { + if (identifier == null) { + return undefined; + } + + const { contact, legacyGroupId, groupMasterKey } = identifier; let convo: ConversationModel | undefined; if (contact) { @@ -1895,7 +1800,7 @@ export async function mergeAccountRecord( } await saveBackupsSubscriberData(backupSubscriberData); - await saveBackupTier(backupTier?.toNumber()); + await saveBackupTier(toNumber(backupTier) ?? undefined); await itemStorage.put( 'displayBadgesOnProfile', @@ -1986,9 +1891,15 @@ export async function mergeAccountRecord( await itemStorage.remove('usernameLinkCorrupted'); } + let { color: linkColor } = usernameLink; + if ( + !isKnownProtoEnumMember(Proto.AccountRecord.UsernameLink.Color, linkColor) + ) { + linkColor = 0; + } + await Promise.all([ - usernameLink.color && - itemStorage.put('usernameLinkColor', usernameLink.color), + itemStorage.put('usernameLinkColor', linkColor), itemStorage.put('usernameLink', { entropy: usernameLink.entropy, serverId: usernameLink.serverId, @@ -2015,7 +1926,7 @@ export async function mergeAccountRecord( ); } - const override = notificationProfileManualOverride; + const override = notificationProfileManualOverride?.override; let overrideToSave: NotificationProfileOverride | undefined; if (override) { if (override.enabled?.id) { @@ -2026,12 +1937,12 @@ export async function mergeAccountRecord( Bytes.toHex(override.enabled.id), 'mergeAccountRecord' ), - endsAtMs: override.enabled.endAtTimestampMs?.toNumber(), + endsAtMs: toNumber(override.enabled.endAtTimestampMs), }, }; } else if (override.disabledAtTimestampMs) { overrideToSave = { - disabledAtMs: override.disabledAtTimestampMs.toNumber(), + disabledAtMs: toNumber(override.disabledAtTimestampMs), enabled: undefined, }; } else { @@ -2114,7 +2025,7 @@ export async function mergeAccountRecord( export async function mergeStoryDistributionListRecord( storageID: string, storageVersion: number, - storyDistributionListRecord: Proto.IStoryDistributionListRecord + storyDistributionListRecord: Proto.StoryDistributionListRecord ): Promise { const redactedStorageID = redactExtendedStorageID({ storageID, @@ -2168,7 +2079,7 @@ export async function mergeStoryDistributionListRecord( remoteListMembers = []; } - if (storyDistributionListRecord.$unknownFields) { + if (storyDistributionListRecord.$unknown) { details.push('adding unknown fields'); } @@ -2188,7 +2099,7 @@ export async function mergeStoryDistributionListRecord( storageID, storageVersion, storageUnknownFields: toStorageUnknownFields( - storyDistributionListRecord.$unknownFields + storyDistributionListRecord.$unknown ), storageNeedsSync: false, }; @@ -2213,7 +2124,7 @@ export async function mergeStoryDistributionListRecord( const oldStorageVersion = localStoryDistributionList.storageVersion; const needsToClearUnknownFields = - !storyDistributionListRecord.$unknownFields && + !storyDistributionListRecord.$unknown && localStoryDistributionList.storageUnknownFields; if (needsToClearUnknownFields) { @@ -2264,7 +2175,7 @@ export async function mergeStoryDistributionListRecord( export async function mergeStickerPackRecord( storageID: string, storageVersion: number, - stickerPackRecord: Proto.IStickerPackRecord + stickerPackRecord: Proto.StickerPackRecord ): Promise { const redactedStorageID = redactExtendedStorageID({ storageID, @@ -2285,18 +2196,18 @@ export async function mergeStickerPackRecord( stickerPackRecord ); - if (stickerPackRecord.$unknownFields) { + if (stickerPackRecord.$unknown) { details.push('adding unknown fields'); } const storageUnknownFields = toStorageUnknownFields( - stickerPackRecord.$unknownFields + stickerPackRecord.$unknown ); let stickerPack: StickerPackInfoType; - if (stickerPackRecord.deletedAtTimestamp?.toNumber()) { + if (toNumber(stickerPackRecord.deletedAtTimestamp)) { stickerPack = { id, - uninstalledAt: stickerPackRecord.deletedAtTimestamp.toNumber(), + uninstalledAt: toNumber(stickerPackRecord.deletedAtTimestamp), storageID, storageVersion, storageUnknownFields, @@ -2394,7 +2305,7 @@ export async function mergeStickerPackRecord( export async function mergeCallLinkRecord( storageID: string, storageVersion: number, - callLinkRecord: Proto.ICallLinkRecord + callLinkRecord: Proto.CallLinkRecord ): Promise { const redactedStorageID = redactExtendedStorageID({ storageID, @@ -2424,7 +2335,7 @@ export async function mergeCallLinkRecord( ); // Note deletedAtTimestampMs can be 0 - const deletedAtTimestampMs = callLinkRecord.deletedAtTimestampMs?.toNumber(); + const deletedAtTimestampMs = toNumber(callLinkRecord.deletedAtTimestampMs); const deletedAt = deletedAtTimestampMs || null; const shouldDrop = Boolean( deletedAt && isOlderThan(deletedAt, getMessageQueueTime()) @@ -2448,7 +2359,7 @@ export async function mergeCallLinkRecord( storageID, storageVersion, - storageUnknownFields: toStorageUnknownFields(callLinkRecord.$unknownFields), + storageUnknownFields: toStorageUnknownFields(callLinkRecord.$unknown), storageNeedsSync: 0, }; @@ -2499,8 +2410,7 @@ export async function mergeCallLinkRecord( const oldStorageVersion = localCallLinkDbRecord.storageVersion || undefined; const needsToClearUnknownFields = - !callLinkRecord.$unknownFields && - localCallLinkDbRecord.storageUnknownFields; + !callLinkRecord.$unknown && localCallLinkDbRecord.storageUnknownFields; if (needsToClearUnknownFields) { details.push('clearing unknown fields'); } @@ -2544,7 +2454,9 @@ export async function mergeCallLinkRecord( }; } -function protoToChatFolderType(folderType: Proto.ChatFolderRecord.FolderType) { +function protoToChatFolderType( + folderType: Proto.ChatFolderRecord['folderType'] +) { if (folderType === Proto.ChatFolderRecord.FolderType.ALL) { return ChatFolderType.ALL; } @@ -2559,26 +2471,30 @@ function recipientToConversationId( logPrefix: string ): string { let match: ConversationModel | undefined; - if (recipient.contact != null) { + if (recipient.identifier?.contact != null) { const serviceId = fromServiceIdBinaryOrString( - recipient.contact.serviceIdBinary, - recipient.contact.serviceId, + recipient.identifier.contact.serviceIdBinary, + recipient.identifier.contact.serviceId, `${logPrefix}.recipientToConversationId` ); match = window.ConversationController.get(serviceId); - match ??= window.ConversationController.get(recipient.contact.e164); + match ??= window.ConversationController.get( + recipient.identifier.contact.e164 + ); } else if ( - recipient.groupMasterKey != null && - recipient.groupMasterKey.byteLength !== 0 + recipient.identifier?.groupMasterKey != null && + recipient.identifier?.groupMasterKey.byteLength !== 0 ) { - const secretParams = deriveGroupSecretParams(recipient.groupMasterKey); + const secretParams = deriveGroupSecretParams( + recipient.identifier.groupMasterKey + ); const groupId = Bytes.toBase64(deriveGroupID(secretParams)); match = window.ConversationController.get(groupId); } else if ( - recipient.legacyGroupId != null && - recipient.legacyGroupId.byteLength !== 0 + recipient.identifier?.legacyGroupId != null && + recipient.identifier.legacyGroupId.byteLength !== 0 ) { - const groupId = Bytes.toBinary(recipient.legacyGroupId); + const groupId = Bytes.toBinary(recipient.identifier.legacyGroupId); match = window.ConversationController.get(groupId); } else { throw new Error('Unexpected type of recipient'); @@ -2599,7 +2515,7 @@ function recipientsToConversationIds( export async function mergeChatFolderRecord( storageID: string, storageVersion: number, - remoteChatFolderRecord: Proto.IChatFolderRecord + remoteChatFolderRecord: Proto.ChatFolderRecord ): Promise { const redactedStorageID = redactExtendedStorageID({ storageID, @@ -2635,12 +2551,12 @@ export async function mergeChatFolderRecord( logPrefix ), deletedAtTimestampMs: - remoteChatFolderRecord.deletedAtTimestampMs?.toNumber() ?? 0, + toNumber(remoteChatFolderRecord.deletedAtTimestampMs) ?? 0, storageID, storageVersion, storageUnknownFields: - remoteChatFolderRecord.$unknownFields != null - ? Bytes.concatenate(remoteChatFolderRecord.$unknownFields) + remoteChatFolderRecord.$unknown != null + ? Bytes.concatenate(remoteChatFolderRecord.$unknown) : null, storageNeedsSync: false, }; @@ -2882,7 +2798,7 @@ export function prepareForEnabledNotificationProfileSync(): { export async function mergeNotificationProfileRecord( storageID: string, storageVersion: number, - profileRecord: Proto.INotificationProfile + profileRecord: Proto.NotificationProfile ): Promise { const redactedStorageID = redactExtendedStorageID({ storageID, @@ -2923,7 +2839,7 @@ export async function mergeNotificationProfileRecord( const localProfile = await DataReader.getNotificationProfileById(idString); // Note deletedAtTimestampMs can be 0 - const deletedAt = deletedAtTimestampMs?.toNumber() || null; + const deletedAt = toNumber(deletedAtTimestampMs) || null; const shouldDrop = Boolean( deletedAt && isOlderThan(deletedAt, getMessageQueueTime()) ); @@ -2950,21 +2866,27 @@ export async function mergeNotificationProfileRecord( name, emoji: dropNull(emoji), color: dropNull(color) ?? DEFAULT_PROFILE_COLOR, - createdAtMs: createdAtMs?.toNumber() ?? Date.now(), + createdAtMs: toNumber(createdAtMs) ?? Date.now(), allowAllCalls: Boolean(allowAllCalls), allowAllMentions: Boolean(allowAllMentions), allowedMembers: new Set(allowedMemberConversationIds), scheduleEnabled: Boolean(scheduleEnabled), scheduleStartTime: dropNull(scheduleStartTime), scheduleEndTime: dropNull(scheduleEndTime), - scheduleDaysEnabled: fromDayOfWeekArray(scheduleDaysEnabled), + scheduleDaysEnabled: fromDayOfWeekArray( + scheduleDaysEnabled.map(day => { + return isKnownProtoEnumMember(Proto.NotificationProfile.DayOfWeek, day) + ? day + : Proto.NotificationProfile.DayOfWeek.UNKNOWN; + }) + ), deletedAtTimestampMs: localDeletedAt ? Math.min(localDeletedAt, deletedAt ?? Number.MAX_SAFE_INTEGER) : dropNull(deletedAt), storageID, storageVersion, storageUnknownFields: - toStorageUnknownFields(profileRecord.$unknownFields) ?? undefined, + toStorageUnknownFields(profileRecord.$unknown) ?? undefined, storageNeedsSync: false, }; @@ -2992,7 +2914,7 @@ export async function mergeNotificationProfileRecord( const oldStorageVersion = localProfile.storageVersion || undefined; const needsToClearUnknownFields = - !profileRecord.$unknownFields && localProfile.storageUnknownFields; + !profileRecord.$unknown && localProfile.storageUnknownFields; if (needsToClearUnknownFields) { details.push('clearing unknown fields'); } diff --git a/ts/sql/cleanDataForIpc.std.ts b/ts/sql/cleanDataForIpc.std.ts index bf11c51f34..30f702680d 100644 --- a/ts/sql/cleanDataForIpc.std.ts +++ b/ts/sql/cleanDataForIpc.std.ts @@ -6,6 +6,8 @@ import { createLogger } from '../logging/log.std.js'; import { isIterable } from '../util/iterables.std.js'; +import { toNumber } from '../util/toNumber.std.js'; + const { isPlainObject } = lodash; const log = createLogger('cleanDataForIpc'); @@ -133,13 +135,10 @@ function cleanDataInner( const dataAsRecord = data as Record; - if ( - 'toNumber' in dataAsRecord && - typeof dataAsRecord.toNumber === 'function' - ) { + if (typeof dataAsRecord === 'bigint') { // We clean this just in case `toNumber` returns something bogus. return cleanDataInner( - dataAsRecord.toNumber(), + toNumber(dataAsRecord), path, pathsChanged, depth + 1 diff --git a/ts/sql/migrations/1280-blob-unprocessed.std.ts b/ts/sql/migrations/1280-blob-unprocessed.std.ts index 0eae1557aa..f337b57fc9 100644 --- a/ts/sql/migrations/1280-blob-unprocessed.std.ts +++ b/ts/sql/migrations/1280-blob-unprocessed.std.ts @@ -9,6 +9,7 @@ import { toTaggedPni, isUntaggedPniString, } from '../../types/ServiceId.std.js'; +import { isKnownProtoEnumMember } from '../../util/isKnownProtoEnumMember.std.js'; import { Migrations as Proto } from '../../protobuf/index.std.js'; import { sql } from '../util.std.js'; import type { WritableDB } from '../Interface.std.js'; @@ -120,7 +121,9 @@ export default function updateToSchemaVersion1280( insertStmt.run({ ...rest, id, - type: decoded.type ?? Proto.Envelope.Type.UNKNOWN, + type: isKnownProtoEnumMember(Proto.Envelope.Type, decoded.type) + ? decoded.type + : Proto.Envelope.Type.UNKNOWN, content: content ?? null, isEncrypted: decrypted ? 0 : 1, timestamp: timestamp || Date.now(), diff --git a/ts/sql/migrations/89-call-history.node.ts b/ts/sql/migrations/89-call-history.node.ts index f7a2896028..3563afad4d 100644 --- a/ts/sql/migrations/89-call-history.node.ts +++ b/ts/sql/migrations/89-call-history.node.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import { callIdFromEra } from '@signalapp/ringrtc'; -import Long from 'long'; import { v4 as generateUuid } from 'uuid'; import lodash from 'lodash'; @@ -156,7 +155,8 @@ function convertLegacyCallDetails( } timestamp = details.acceptedTime ?? details.endedTime ?? fallbackTimestamp; } else if (mode === CallMode.Group) { - callId = Long.fromValue(callIdFromEra(details.eraId)).toString(); + const callIdBigInt: bigint = callIdFromEra(details.eraId); + callId = callIdBigInt.toString(); type = CallType.Group; direction = details.creatorUuid === ourUuid diff --git a/ts/state/ducks/calling.preload.ts b/ts/state/ducks/calling.preload.ts index 3347b59d49..e16f4804bc 100644 --- a/ts/state/ducks/calling.preload.ts +++ b/ts/state/ducks/calling.preload.ts @@ -4,7 +4,6 @@ import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; import { ipcRenderer } from 'electron'; import lodash from 'lodash'; -import Long from 'long'; import type { ReadonlyDeep } from 'type-fest'; import { CallLinkRootKey, @@ -2971,24 +2970,24 @@ function submitCallQualitySurvey( callQualityIssues.includes(CallQualitySurvey.Issue.OTHER) ? additionalIssuesDescription : null, - debugLogUrl, - startTimestamp: Long.fromNumber(callSummary.startTime), - endTimestamp: Long.fromNumber(callSummary.endTime), + debugLogUrl: debugLogUrl ?? null, + startTimestamp: BigInt(callSummary.startTime), + endTimestamp: BigInt(callSummary.endTime), callType, success: !isCallFailure(callSummary.callEndReasonText), callEndReason: callSummary.callEndReasonText, - connectionRttMedian: qualityStats.rttMedianConnectionMillis, - audioRttMedian: audioStats.rttMedianMillis, - videoRttMedian: videoStats.rttMedianMillis, - audioRecvJitterMedian: audioStats.jitterMedianRecvMillis, - videoRecvJitterMedian: videoStats.jitterMedianRecvMillis, - audioSendJitterMedian: audioStats.jitterMedianSendMillis, - videoSendJitterMedian: videoStats.jitterMedianSendMillis, - audioRecvPacketLossFraction: audioStats.packetLossFractionRecv, - videoRecvPacketLossFraction: videoStats.packetLossFractionRecv, - audioSendPacketLossFraction: audioStats.packetLossFractionSend, - videoSendPacketLossFraction: videoStats.packetLossFractionSend, - callTelemetry: callSummary.rawStats, + connectionRttMedian: qualityStats.rttMedianConnectionMillis ?? null, + audioRttMedian: audioStats.rttMedianMillis ?? null, + videoRttMedian: videoStats.rttMedianMillis ?? null, + audioRecvJitterMedian: audioStats.jitterMedianRecvMillis ?? null, + videoRecvJitterMedian: videoStats.jitterMedianRecvMillis ?? null, + audioSendJitterMedian: audioStats.jitterMedianSendMillis ?? null, + videoSendJitterMedian: videoStats.jitterMedianSendMillis ?? null, + audioRecvPacketLossFraction: audioStats.packetLossFractionRecv ?? null, + videoRecvPacketLossFraction: videoStats.packetLossFractionRecv ?? null, + audioSendPacketLossFraction: audioStats.packetLossFractionSend ?? null, + videoSendPacketLossFraction: videoStats.packetLossFractionSend ?? null, + callTelemetry: callSummary.rawStats ?? null, }; await submitCallQualitySurveyToServer(surveyRequest); diff --git a/ts/test-electron/ContactsParser_test.preload.ts b/ts/test-electron/ContactsParser_test.preload.ts index 86c45bf600..0f6ce7a1f2 100644 --- a/ts/test-electron/ContactsParser_test.preload.ts +++ b/ts/test-electron/ContactsParser_test.preload.ts @@ -15,7 +15,6 @@ import { pipeline } from 'node:stream/promises'; import { Transform } from 'node:stream'; import fse from 'fs-extra'; -import protobuf from '../protobuf/wrap.std.js'; import { createLogger } from '../logging/log.std.js'; import * as Bytes from '../Bytes.std.js'; import * as Errors from '../types/errors.std.js'; @@ -33,6 +32,7 @@ import { } from '../textsecure/ContactsParser.preload.js'; import type { ContactDetailsWithAvatar } from '../textsecure/ContactsParser.preload.js'; import { strictAssert } from '../util/assert.std.js'; +import { encodeDelimited } from '../util/encodeDelimited.std.js'; import { toAciObject } from '../util/ServiceId.node.js'; import { generateKeys, @@ -41,8 +41,6 @@ import { const log = createLogger('ContactsParser_test'); -const { Writer } = protobuf; - const DEFAULT_ACI = generateAci(); describe('ContactsParser', () => { @@ -115,7 +113,7 @@ describe('ContactsParser', () => { try { const bytes = Bytes.concatenate([ - generatePrefixedContact(undefined), + ...generatePrefixedContact(undefined), getTestBuffer(), ]); @@ -195,7 +193,7 @@ function getTestBuffer(): Uint8Array { const chunks: Array = []; for (let i = 0; i < 3; i += 1) { - chunks.push(prefixedContact); + chunks.push(...prefixedContact); chunks.push(avatarBuffer); } @@ -205,20 +203,21 @@ function getTestBuffer(): Uint8Array { function generatePrefixedContact( avatarBuffer: Uint8Array | undefined, aci: AciString | null = DEFAULT_ACI -) { +): [Uint8Array, Uint8Array] { const contactInfoBuffer = Proto.ContactDetails.encode({ name: 'Zero Cool', number: '+10000000000', + aci: null, aciBinary: aci == null ? null : toAciObject(aci).getRawUuidBytes(), avatar: avatarBuffer ? { contentType: 'image/jpeg', length: avatarBuffer.length } - : undefined, - }).finish(); + : null, + expireTimer: null, + expireTimerVersion: null, + inboxPosition: null, + }); - const writer = new Writer(); - writer.bytes(contactInfoBuffer); - const prefixedContact = writer.finish(); - return prefixedContact; + return encodeDelimited(contactInfoBuffer); } async function verifyContact( diff --git a/ts/test-electron/MessageReceiver_test.preload.ts b/ts/test-electron/MessageReceiver_test.preload.ts index 1ec270cd63..e0ecb1c9ce 100644 --- a/ts/test-electron/MessageReceiver_test.preload.ts +++ b/ts/test-electron/MessageReceiver_test.preload.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; -import Long from 'long'; import MessageReceiver from '../textsecure/MessageReceiver.preload.js'; import { @@ -52,11 +51,23 @@ describe('MessageReceiver', () => { const body = Proto.Envelope.encode({ type: Proto.Envelope.Type.DOUBLE_RATCHET, + sourceServiceId: null, sourceServiceIdBinary: toAciObject(someAci).getRawUuidBytes(), sourceDeviceId: deviceId, - clientTimestamp: Long.fromNumber(Date.now()), + clientTimestamp: BigInt(Date.now()), content: Crypto.getRandomBytes(200), - }).finish(); + destinationServiceId: null, + destinationServiceIdBinary: null, + serverGuid: null, + serverGuidBinary: null, + serverTimestamp: null, + ephemeral: null, + urgent: null, + updatedPni: null, + story: null, + reportSpamToken: null, + updatedPniBinary: null, + }); messageReceiver.handleRequest( new IncomingWebSocketRequest( diff --git a/ts/test-electron/SignalProtocolStore_test.preload.ts b/ts/test-electron/SignalProtocolStore_test.preload.ts index 385f0b454e..3494edf479 100644 --- a/ts/test-electron/SignalProtocolStore_test.preload.ts +++ b/ts/test-electron/SignalProtocolStore_test.preload.ts @@ -42,13 +42,6 @@ import { itemStorage } from '../textsecure/Storage.preload.js'; const { clone } = lodash; -const { - RecordStructure, - SessionStructure, - SenderKeyRecordStructure, - SenderKeyStateStructure, -} = signal.proto.storage; - const ZERO = new Uint8Array(0); describe('SignalProtocolStore', () => { @@ -84,24 +77,34 @@ describe('SignalProtocolStore', () => { }; function getSessionRecord(isOpen?: boolean): SessionRecord { - const proto = new RecordStructure(); - - proto.previousSessions = []; + const proto: signal.proto.storage.RecordStructure.Params = { + previousSessions: [], + currentSession: null, + }; if (isOpen) { - proto.currentSession = new SessionStructure(); + proto.currentSession = { + aliceBaseKey: getPublicKey(), + localIdentityPublic: getPublicKey(), + localRegistrationId: 435, - proto.currentSession.aliceBaseKey = getPublicKey(); - proto.currentSession.localIdentityPublic = getPublicKey(); - proto.currentSession.localRegistrationId = 435; + previousCounter: 1, + remoteIdentityPublic: getPublicKey(), + remoteRegistrationId: 243, - proto.currentSession.previousCounter = 1; - proto.currentSession.remoteIdentityPublic = getPublicKey(); - proto.currentSession.remoteRegistrationId = 243; + rootKey: getPrivateKey(), + sessionVersion: 3, + senderChain: { + senderRatchetKey: null, + senderRatchetKeyPrivate: null, + chainKey: null, + messageKeys: null, + }, - proto.currentSession.rootKey = getPrivateKey(); - proto.currentSession.sessionVersion = 3; - proto.currentSession.senderChain = {}; + receiverChains: null, + pendingPreKey: null, + needsRefresh: null, + }; } return SessionRecord.deserialize( @@ -110,37 +113,30 @@ describe('SignalProtocolStore', () => { } function getSenderKeyRecord(): SenderKeyRecord { - const proto = new SenderKeyRecordStructure(); - - const state = new SenderKeyStateStructure(); - - state.senderKeyId = 4; - - const senderChainKey = new SenderKeyStateStructure.SenderChainKey(); - - senderChainKey.iteration = 10; - senderChainKey.seed = getPublicKey(); - state.senderChainKey = senderChainKey; - - const senderSigningKey = new SenderKeyStateStructure.SenderSigningKey(); - senderSigningKey.public = getPublicKey(); - senderSigningKey.private = getPrivateKey(); - - state.senderSigningKey = senderSigningKey; - - state.senderMessageKeys = []; - const messageKey = new SenderKeyStateStructure.SenderMessageKey(); - messageKey.iteration = 234; - messageKey.seed = getPublicKey(); - state.senderMessageKeys.push(messageKey); - - proto.senderKeyStates = []; - proto.senderKeyStates.push(state); + const proto: signal.proto.storage.SenderKeyRecordStructure.Params = { + senderKeyStates: [ + { + senderKeyId: 4, + senderChainKey: { + iteration: 10, + seed: getPublicKey(), + }, + senderSigningKey: { + public: getPublicKey(), + private: getPrivateKey(), + }, + senderMessageKeys: [ + { + iteration: 234, + seed: getPublicKey(), + }, + ], + }, + ], + }; return SenderKeyRecord.deserialize( - Buffer.from( - signal.proto.storage.SenderKeyRecordStructure.encode(proto).finish() - ) + Buffer.from(signal.proto.storage.SenderKeyRecordStructure.encode(proto)) ); } diff --git a/ts/test-electron/backup/filePointer_test.preload.ts b/ts/test-electron/backup/filePointer_test.preload.ts index 16b5095ffb..eb82f55343 100644 --- a/ts/test-electron/backup/filePointer_test.preload.ts +++ b/ts/test-electron/backup/filePointer_test.preload.ts @@ -1,14 +1,13 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; -import Long from 'long'; import * as sinon from 'sinon'; import { BackupLevel } from '@signalapp/libsignal-client/zkgroup.js'; import { randomBytes } from 'node:crypto'; import { join } from 'node:path'; import { emptyDir, ensureFile } from 'fs-extra'; -import { Backups } from '../../protobuf/index.std.js'; +import type { Backups } from '../../protobuf/index.std.js'; import { getFilePointerForAttachment, @@ -29,6 +28,7 @@ import { sha256 } from '../../Crypto.node.js'; describe('convertFilePointerToAttachment', () => { const commonFilePointerProps = { + $unknown: [], contentType: 'image/png', width: 100, height: 100, @@ -37,6 +37,7 @@ describe('convertFilePointerToAttachment', () => { caption: 'caption', incrementalMac: Bytes.fromString('incrementalMac'), incrementalMacChunkSize: 1000, + locatorInfo: null, }; const commonAttachmentProps = { contentType: IMAGE_PNG, @@ -52,10 +53,10 @@ describe('convertFilePointerToAttachment', () => { describe('locatorInfo', () => { it('processes filepointer with empty locatorInfo', () => { const result = convertFilePointerToAttachment( - new Backups.FilePointer({ + { ...commonFilePointerProps, - locatorInfo: {}, - }), + locatorInfo: null, + }, { type: 'remote' }, { _createName: () => 'downloadPath' } ); @@ -69,7 +70,7 @@ describe('convertFilePointerToAttachment', () => { }); it('processes filepointer with missing locatorInfo', () => { const result = convertFilePointerToAttachment( - new Backups.FilePointer(commonFilePointerProps), + commonFilePointerProps, { type: 'remote' }, { _createName: () => 'downloadPath' } ); @@ -84,18 +85,22 @@ describe('convertFilePointerToAttachment', () => { it('processes locatorInfo with plaintextHash', () => { const result = convertFilePointerToAttachment( - new Backups.FilePointer({ + { ...commonFilePointerProps, locatorInfo: { + $unknown: [], transitCdnKey: 'cdnKey', transitCdnNumber: 42, size: 128, - transitTierUploadTimestamp: Long.fromNumber(12345), + transitTierUploadTimestamp: 12345n, key: Bytes.fromString('key'), - plaintextHash: Bytes.fromString('plaintextHash'), + integrityCheck: { + plaintextHash: Bytes.fromString('plaintextHash'), + }, mediaTierCdnNumber: 43, + localKey: null, }, - }), + }, { type: 'remote' }, { _createName: () => 'downloadPath' } ); @@ -117,19 +122,22 @@ describe('convertFilePointerToAttachment', () => { }); it('processes locatorInfo with localKey', () => { const result = convertFilePointerToAttachment( - new Backups.FilePointer({ + { ...commonFilePointerProps, locatorInfo: { + $unknown: [], transitCdnKey: 'cdnKey', transitCdnNumber: 42, size: 128, - transitTierUploadTimestamp: Long.fromNumber(12345), + transitTierUploadTimestamp: 12345n, key: Bytes.fromString('key'), - plaintextHash: Bytes.fromString('plaintextHash'), + integrityCheck: { + plaintextHash: Bytes.fromString('plaintextHash'), + }, mediaTierCdnNumber: 43, localKey: Bytes.fromString('localKey'), }, - }), + }, { type: 'local-encrypted', localBackupSnapshotDir: '/root/backups' }, { _createName: () => 'downloadPath', @@ -197,7 +205,7 @@ const defaultMediaName = Bytes.toHex( ]) ); -const defaultFilePointer = new Backups.FilePointer({ +const defaultFilePointer: Backups.FilePointer.Params = { contentType: IMAGE_PNG, width: 100, height: 100, @@ -206,9 +214,8 @@ const defaultFilePointer = new Backups.FilePointer({ caption: 'caption', incrementalMac: Bytes.fromBase64('incrementalMac'), incrementalMacChunkSize: 1000, -}); -const { FilePointer } = Backups; -const { LocatorInfo } = FilePointer; + locatorInfo: null, +}; const notInBackupCdn: GetBackupCdnInfoType = async () => { return { isInBackupTier: false }; @@ -254,17 +261,21 @@ describe('getFilePointerForAttachment', () => { strictAssert(key, 'key exists'); assert.isTrue(isValidAttachmentKey(Bytes.toBase64(key))); - assert.deepStrictEqual( - filePointer, - new FilePointer({ - ...defaultFilePointer, - locatorInfo: new LocatorInfo({ - size: 100, + assert.deepStrictEqual(filePointer, { + ...defaultFilePointer, + locatorInfo: { + size: 100, + integrityCheck: { plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), - key: filePointer.locatorInfo?.key, - }), - }) - ); + }, + key: filePointer.locatorInfo?.key ?? null, + transitCdnKey: null, + transitCdnNumber: null, + transitTierUploadTimestamp: null, + mediaTierCdnNumber: null, + localKey: null, + }, + }); }); it('includes transit cdn info', async () => { @@ -280,17 +291,21 @@ describe('getFilePointerForAttachment', () => { messageReceivedAt: 100, }), { - filePointer: new FilePointer({ + filePointer: { ...defaultFilePointer, - locatorInfo: new LocatorInfo({ - encryptedDigest: Bytes.fromBase64(defaultAttachment.digest), + locatorInfo: { + integrityCheck: { + encryptedDigest: Bytes.fromBase64(defaultAttachment.digest), + }, key: Bytes.fromBase64(defaultAttachment.key), size: 100, transitCdnKey: 'cdnKey', transitCdnNumber: 2, - transitTierUploadTimestamp: Long.fromNumber(1234), - }), - }), + transitTierUploadTimestamp: 1234n, + mediaTierCdnNumber: null, + localKey: null, + }, + }, backupJob: undefined, } ); @@ -308,17 +323,21 @@ describe('getFilePointerForAttachment', () => { messageReceivedAt: 100, }), { - filePointer: new FilePointer({ + filePointer: { ...defaultFilePointer, - locatorInfo: new LocatorInfo({ - plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + locatorInfo: { + integrityCheck: { + plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + }, key: Bytes.fromBase64(defaultAttachment.key), size: 100, transitCdnKey: 'cdnKey', transitCdnNumber: 2, - transitTierUploadTimestamp: Long.fromNumber(1234), - }), - }), + transitTierUploadTimestamp: 1234n, + mediaTierCdnNumber: null, + localKey: null, + }, + }, backupJob: undefined, } ); @@ -337,17 +356,21 @@ describe('getFilePointerForAttachment', () => { messageReceivedAt: 100, }), { - filePointer: new FilePointer({ + filePointer: { ...defaultFilePointer, - locatorInfo: new LocatorInfo({ - plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + locatorInfo: { + integrityCheck: { + plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + }, key: Bytes.fromBase64(defaultAttachment.key), size: 100, transitCdnKey: 'cdnKey', transitCdnNumber: 2, - transitTierUploadTimestamp: Long.fromNumber(1234), - }), - }), + transitTierUploadTimestamp: 1234n, + mediaTierCdnNumber: null, + localKey: null, + }, + }, backupJob: undefined, } ); @@ -366,14 +389,21 @@ describe('getFilePointerForAttachment', () => { messageReceivedAt: 100, }), { - filePointer: new FilePointer({ + filePointer: { ...defaultFilePointer, - locatorInfo: new LocatorInfo({ - plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + locatorInfo: { + integrityCheck: { + plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + }, key: Bytes.fromBase64(defaultAttachment.key), size: 100, - }), - }), + transitCdnKey: null, + transitCdnNumber: null, + transitTierUploadTimestamp: null, + mediaTierCdnNumber: null, + localKey: null, + }, + }, backupJob: undefined, } ); @@ -391,17 +421,21 @@ describe('getFilePointerForAttachment', () => { messageReceivedAt: 100, }), { - filePointer: new FilePointer({ + filePointer: { ...defaultFilePointer, - locatorInfo: new LocatorInfo({ - plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + locatorInfo: { + integrityCheck: { + plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + }, key: Bytes.fromBase64(defaultAttachment.key), size: 100, transitCdnKey: 'cdnKey', transitCdnNumber: 2, - transitTierUploadTimestamp: Long.fromNumber(1234), - }), - }), + transitTierUploadTimestamp: 1234n, + mediaTierCdnNumber: null, + localKey: null, + }, + }, backupJob: { data: { contentType: defaultAttachment.contentType, @@ -436,17 +470,21 @@ describe('getFilePointerForAttachment', () => { messageReceivedAt: 100, }), { - filePointer: new FilePointer({ + filePointer: { ...defaultFilePointer, - locatorInfo: new LocatorInfo({ - plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + locatorInfo: { + integrityCheck: { + plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + }, key: Bytes.fromBase64(defaultAttachment.key), size: 100, transitCdnKey: 'cdnKey', transitCdnNumber: 2, - transitTierUploadTimestamp: Long.fromNumber(1234), - }), - }), + transitTierUploadTimestamp: 1234n, + mediaTierCdnNumber: null, + localKey: null, + }, + }, backupJob: undefined, } ); @@ -473,18 +511,21 @@ describe('getFilePointerForAttachment', () => { messageReceivedAt: 100, }), { - filePointer: new FilePointer({ + filePointer: { ...defaultFilePointer, - locatorInfo: new LocatorInfo({ - plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + locatorInfo: { + integrityCheck: { + plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + }, localKey: Bytes.fromBase64(defaultAttachment.localKey), key: Bytes.fromBase64(defaultAttachment.key), size: 100, transitCdnKey: 'cdnKey', transitCdnNumber: 2, - transitTierUploadTimestamp: Long.fromNumber(1234), - }), - }), + transitTierUploadTimestamp: 1234n, + mediaTierCdnNumber: null, + }, + }, backupJob: { isPlaintextExport: false, data: { @@ -512,17 +553,21 @@ describe('getFilePointerForAttachment', () => { messageReceivedAt: 100, }), { - filePointer: new FilePointer({ + filePointer: { ...defaultFilePointer, - locatorInfo: new LocatorInfo({ - plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + locatorInfo: { + integrityCheck: { + plaintextHash: Bytes.fromHex(defaultAttachment.plaintextHash), + }, key: Bytes.fromBase64(defaultAttachment.key), size: 100, transitCdnKey: 'cdnKey', transitCdnNumber: 2, - transitTierUploadTimestamp: Long.fromNumber(1234), - }), - }), + transitTierUploadTimestamp: 1234n, + mediaTierCdnNumber: null, + localKey: null, + }, + }, backupJob: undefined, } ); diff --git a/ts/test-electron/backup/non_bubble_test.preload.ts b/ts/test-electron/backup/non_bubble_test.preload.ts index f5b1e5e2c1..3f49742cf8 100644 --- a/ts/test-electron/backup/non_bubble_test.preload.ts +++ b/ts/test-electron/backup/non_bubble_test.preload.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import { v4 as generateGuid } from 'uuid'; -import Long from 'long'; import type { ConversationModel } from '../../models/conversations.preload.js'; @@ -258,12 +257,11 @@ describe('backup/non-bubble messages', () => { kind: PaymentEventKind.Activation, }, received_at: 1, - received_at_ms: 1, sent_at: 1, timestamp: 1, - readStatus: ReadStatus.Unread, - seenStatus: SeenStatus.Unseen, - unidentifiedDeliveryReceived: true, + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Seen, + unidentifiedDeliveryReceived: false, }, ]); }); @@ -279,12 +277,11 @@ describe('backup/non-bubble messages', () => { kind: PaymentEventKind.ActivationRequest, }, received_at: 1, - received_at_ms: 1, sent_at: 1, timestamp: 1, - readStatus: ReadStatus.Unread, - seenStatus: SeenStatus.Unseen, - unidentifiedDeliveryReceived: true, + readStatus: ReadStatus.Read, + seenStatus: SeenStatus.Seen, + unidentifiedDeliveryReceived: false, }, ]); }); @@ -334,10 +331,18 @@ describe('backup/non-bubble messages', () => { feeMob: '0.01', transactionDetailsBase64: Bytes.toBase64( Backups.PaymentNotification.TransactionDetails.encode({ - transaction: { - timestamp: Long.fromNumber(Date.now()), + payment: { + transaction: { + timestamp: BigInt(Date.now()), + status: null, + mobileCoinIdentification: null, + blockIndex: null, + blockTimestamp: null, + transaction: null, + receipt: null, + }, }, - }).finish() + }) ), }, }, diff --git a/ts/test-helpers/generateBackup.node.ts b/ts/test-helpers/generateBackup.node.ts index b3270798d8..526d7c4fa5 100644 --- a/ts/test-helpers/generateBackup.node.ts +++ b/ts/test-helpers/generateBackup.node.ts @@ -4,8 +4,6 @@ import { Readable } from 'node:stream'; import { createGzip } from 'node:zlib'; import { createCipheriv, randomBytes } from 'node:crypto'; -import { Buffer } from 'node:buffer'; -import Long from 'long'; import { AccountEntropyPool, BackupKey, @@ -19,6 +17,7 @@ import { appendPaddingStream } from '../util/logPadding.node.js'; import { prependStream } from '../util/prependStream.node.js'; import { appendMacStream } from '../util/appendMacStream.node.js'; import { toAciObject } from '../util/ServiceId.node.js'; +import { encodeDelimited } from '../util/encodeDelimited.std.js'; import { BACKUP_VERSION } from '../services/backups/constants.std.js'; import { Backups } from '../protobuf/index.std.js'; @@ -76,14 +75,20 @@ export function generateBackup( return { backupId, stream }; } -function frame(data: Backups.IFrame): Buffer { - return Buffer.from(Backups.Frame.encodeDelimited(data).finish()); +function* frame( + item: NonNullable +): Iterable { + yield* encodeDelimited( + Backups.Frame.encode({ + item, + }) + ); } let now = Date.now(); -function getTimestamp(): Long { +function getTimestamp(): bigint { now += 1; - return Long.fromNumber(now); + return BigInt(now); } function* createRecords({ @@ -92,17 +97,20 @@ function* createRecords({ conversationAcis = [], messages, mediaRootBackupKey, -}: BackupGeneratorConfigType): Iterable { - yield Buffer.from( - Backups.BackupInfo.encodeDelimited({ - version: Long.fromNumber(BACKUP_VERSION), +}: BackupGeneratorConfigType): Iterable { + yield* encodeDelimited( + Backups.BackupInfo.encode({ + version: BigInt(BACKUP_VERSION), backupTimeMs: getTimestamp(), mediaRootBackupKey, - }).finish() + currentAppVersion: null, + firstAppVersion: null, + debugInfo: null, + }) ); // Account data - yield frame({ + yield* frame({ account: { profileKey, givenName: 'Backup', @@ -127,30 +135,58 @@ function* createRecords({ phoneNumberSharingMode: Backups.AccountData.PhoneNumberSharingMode.EVERYBODY, defaultChatStyle: { - autoBubbleColor: {}, + wallpaper: null, + bubbleColor: { + autoBubbleColor: {}, + }, dimWallpaperInDarkMode: false, }, customChatColors: [], + storyViewReceiptsEnabled: null, + optimizeOnDeviceStorage: null, + backupTier: null, + defaultSentMediaQuality: null, + autoDownloadSettings: null, + screenLockTimeoutMinutes: null, + pinReminders: null, + appTheme: null, + callsUseLessDataSetting: null, + allowSealedSenderFromAnyone: null, + allowAutomaticKeyVerification: null, + }, + username: null, + usernameLink: null, + avatarUrlPath: null, + donationSubscriberData: null, + backupsSubscriberData: null, + svrPin: null, + androidSpecificSettings: null, + bioText: null, + bioEmoji: null, + keyTransparencyData: null, + }, + }); + + const selfId = 0n; + + yield* frame({ + recipient: { + id: selfId, + destination: { + self: { + avatarColor: null, + }, }, }, }); - const selfId = Long.fromNumber(0); - - yield frame({ - recipient: { - id: selfId, - self: {}, - }, - }); - const chats = new Array<{ - id: Long; + id: bigint; aci: Uint8Array; }>(); for (let i = 1; i <= conversations; i += 1) { - const id = Long.fromNumber(i); + const id = BigInt(i); const chatAci = toAciObject( conversationAcis.at(i - 1) ?? generateAci() ).getRawUuidBytes(); @@ -160,35 +196,55 @@ function* createRecords({ aci: chatAci, }); - yield frame({ + yield* frame({ recipient: { id, - contact: { - aci: chatAci, - blocked: false, - visibility: Backups.Contact.Visibility.VISIBLE, - registered: {}, - profileKey: randomBytes(32), - profileSharing: true, - profileGivenName: `Contact ${i}`, - profileFamilyName: 'Generated', - hideStory: false, + destination: { + contact: { + aci: chatAci, + blocked: false, + visibility: Backups.Contact.Visibility.VISIBLE, + registration: { + registered: {}, + }, + profileKey: randomBytes(32), + profileSharing: true, + profileGivenName: `Contact ${i}`, + profileFamilyName: 'Generated', + hideStory: false, + + pni: null, + username: null, + e164: null, + identityKey: null, + identityState: null, + nickname: null, + note: null, + systemGivenName: null, + systemFamilyName: null, + systemNickname: null, + avatarColor: null, + keyTransparencyData: null, + }, }, }, }); - yield frame({ + yield* frame({ chat: { id, recipientId: id, archived: false, pinnedOrder: 0, - expirationTimerMs: Long.fromNumber(0), - muteUntilMs: Long.fromNumber(0), + expirationTimerMs: 0n, + muteUntilMs: 0n, markedUnread: false, dontNotifyForMentionsIfMuted: false, style: { - autoBubbleColor: {}, + wallpaper: null, + bubbleColor: { + autoBubbleColor: {}, + }, dimWallpaperInDarkMode: false, }, expireTimerVersion: 1, @@ -203,15 +259,18 @@ function* createRecords({ const dateSent = getTimestamp(); - yield frame({ + yield* frame({ chatItem: { chatId: chat.id, authorId: isIncoming ? chat.id : selfId, dateSent, revisions: [], sms: false, + expireStartDate: null, + expiresInMs: null, + pinDetails: null, - ...(isIncoming + directionalDetails: isIncoming ? { incoming: { dateReceived: getTimestamp(), @@ -222,19 +281,30 @@ function* createRecords({ } : { outgoing: { + dateReceived: getTimestamp(), sendStatus: [ { recipientId: chat.id, timestamp: dateSent, - delivered: { sealedSender: true }, + deliveryStatus: { + delivered: { sealedSender: true }, + }, }, ], }, - }), + }, - standardMessage: { - text: { - body: `Message ${i}`, + item: { + standardMessage: { + text: { + body: `Message ${i}`, + bodyRanges: null, + }, + quote: null, + attachments: null, + linkPreview: null, + longText: null, + reactions: null, }, }, }, diff --git a/ts/test-mock/pnp/accept_gv2_invite_test.node.ts b/ts/test-mock/pnp/accept_gv2_invite_test.node.ts index 075ae64511..7d1e643dad 100644 --- a/ts/test-mock/pnp/accept_gv2_invite_test.node.ts +++ b/ts/test-mock/pnp/accept_gv2_invite_test.node.ts @@ -232,7 +232,7 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { const actions = Proto.GroupChange.Actions.decode( groupChange?.actions ?? new Uint8Array(0) ); - assert.strictEqual(actions.deletePendingMembers.length, 1); + assert.strictEqual(actions.deleteMembersPendingProfileKey.length, 1); }); it('should accept ACI invite with extra PNI on the invite list', async () => { diff --git a/ts/test-node/Proto_unknown_field_test.std.ts b/ts/test-node/Proto_unknown_field_test.std.ts deleted file mode 100644 index ff74bc914a..0000000000 --- a/ts/test-node/Proto_unknown_field_test.std.ts +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { assert } from 'chai'; -import { Root } from 'protobufjs'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const { Partial, Full } = (Root as any).fromJSON({ - nested: { - test: { - nested: { - Partial: { - fields: { - a: { - type: 'string', - id: 1, - }, - c: { - type: 'int32', - id: 3, - }, - }, - }, - Full: { - fields: { - a: { - type: 'string', - id: 1, - }, - b: { - type: 'bool', - id: 2, - }, - c: { - type: 'int32', - id: 3, - }, - d: { - type: 'bytes', - id: 4, - }, - }, - }, - }, - }, - }, -}).nested.test; - -describe('Proto#$unknownFields', () => { - it('should encode and decode with unknown fields', () => { - const full = Full.encode({ - a: 'hello', - b: true, - c: 42, - d: Buffer.from('ohai'), - }).finish(); - - const partial = Partial.decode(full); - assert.strictEqual(partial.a, 'hello'); - assert.strictEqual(partial.c, 42); - assert.strictEqual(partial.$unknownFields.length, 2); - assert.strictEqual( - Buffer.from(partial.$unknownFields[0]).toString('hex'), - '1001' - ); - assert.strictEqual( - Buffer.from(partial.$unknownFields[1]).toString('hex'), - '22046f686169' - ); - - const encoded = Partial.encode({ - a: partial.a, - c: partial.c, - $unknownFields: partial.$unknownFields, - }).finish(); - const decoded = Full.decode(encoded); - - assert.strictEqual(decoded.a, 'hello'); - assert.strictEqual(decoded.b, true); - assert.strictEqual(decoded.c, 42); - assert.strictEqual(Buffer.from(decoded.d).toString(), 'ohai'); - - const concat = Partial.encode({ - a: partial.a, - c: partial.c, - $unknownFields: [Buffer.concat(partial.$unknownFields)], - }).finish(); - const decodedConcat = Full.decode(concat); - - assert.strictEqual(decodedConcat.a, 'hello'); - assert.isTrue(decodedConcat.b); - assert.strictEqual(decodedConcat.c, 42); - assert.strictEqual(Buffer.from(decodedConcat.d).toString(), 'ohai'); - }); - - it('should decode unknown fields before reencoding them', () => { - const full = Full.encode({ - a: 'hello', - b: true, - c: 42, - d: Buffer.from('ohai'), - }).finish(); - - const partial = Partial.decode(full); - assert.isUndefined(partial.b); - - const encoded = Full.encode({ - ...partial, - b: false, - }).finish(); - const decoded = Full.decode(encoded); - - assert.strictEqual(decoded.a, 'hello'); - assert.isFalse(decoded.b); - assert.strictEqual(decoded.c, 42); - assert.strictEqual(Buffer.from(decoded.d).toString(), 'ohai'); - }); - - it('should not set unknown fields if all fields were known', () => { - const encoded = Partial.encode({ - a: 'hello', - c: 42, - }).finish(); - const decoded = Full.decode(encoded); - - assert.strictEqual(decoded.a, 'hello'); - assert.strictEqual(decoded.c, 42); - assert.isUndefined(decoded.$unknownFields); - - const encodedWithEmptyArray = Partial.encode({ - a: 'hi', - c: 69, - $unkownFields: [], - }).finish(); - const decodedWithEmptyArray = Full.decode(encodedWithEmptyArray); - - assert.strictEqual(decodedWithEmptyArray.a, 'hi'); - assert.strictEqual(decodedWithEmptyArray.c, 69); - assert.isUndefined(decodedWithEmptyArray.$unknownFields); - }); -}); diff --git a/ts/test-node/groups/add_banned_member_test.preload.ts b/ts/test-node/groups/add_banned_member_test.preload.ts index 6ba994daac..aa4f16b6b6 100644 --- a/ts/test-node/groups/add_banned_member_test.preload.ts +++ b/ts/test-node/groups/add_banned_member_test.preload.ts @@ -99,8 +99,8 @@ describe('group add banned member', () => { }, }); - assert.isUndefined(actions.addMembersBanned); - assert.isUndefined(actions.deleteMembersBanned); + assert.isNull(actions.addMembersBanned); + assert.isNull(actions.deleteMembersBanned); }); it('should not ban already banned person', () => { @@ -113,7 +113,7 @@ describe('group add banned member', () => { }, }); - assert.isUndefined(actions.addMembersBanned); - assert.isUndefined(actions.deleteMembersBanned); + assert.isNull(actions.addMembersBanned); + assert.isNull(actions.deleteMembersBanned); }); }); diff --git a/ts/test-node/processDataMessage_test.preload.ts b/ts/test-node/processDataMessage_test.preload.ts index e6a6c22b5b..d743a92104 100644 --- a/ts/test-node/processDataMessage_test.preload.ts +++ b/ts/test-node/processDataMessage_test.preload.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; -import Long from 'long'; import { v4 as generateUuid } from 'uuid'; import { @@ -23,9 +22,40 @@ const FLAGS = Proto.DataMessage.Flags; const TIMESTAMP = Date.now(); const CLIENT_UUID = generateUuid(); -const UNPROCESSED_ATTACHMENT: Proto.IAttachmentPointer = { - cdnId: Long.fromNumber(123), - cdnKey: 'cdnKey', +const EMPTY_DATA_MESSAGE: Proto.DataMessage.Params = { + body: null, + attachments: null, + groupV2: null, + flags: null, + expireTimer: null, + expireTimerVersion: null, + profileKey: null, + timestamp: null, + quote: null, + contact: null, + preview: null, + sticker: null, + requiredProtocolVersion: null, + isViewOnce: null, + reaction: null, + delete: null, + bodyRanges: null, + groupCallUpdate: null, + payment: null, + storyContext: null, + giftBadge: null, + pollCreate: null, + pollTerminate: null, + pollVote: null, + pinMessage: null, + unpinMessage: null, + adminDelete: null, +}; + +const UNPROCESSED_ATTACHMENT: Proto.AttachmentPointer.Params = { + attachmentIdentifier: { + cdnKey: 'cdnKey', + }, cdnNumber: 2, blurHash: 'blurHash', caption: 'caption', @@ -35,16 +65,17 @@ const UNPROCESSED_ATTACHMENT: Proto.IAttachmentPointer = { contentType: IMAGE_GIF, incrementalMac: new Uint8Array([12, 12, 12]), chunkSize: 24, - uploadTimestamp: Long.fromNumber(456), + uploadTimestamp: 456n, size: 34, height: 64, width: 128, flags: 1, fileName: 'fileName', + thumbnail: null, }; const PROCESSED_ATTACHMENT: ProcessedAttachment = { - cdnId: '123', + cdnId: undefined, cdnKey: 'cdnKey', cdnNumber: 2, blurHash: 'blurHash', @@ -64,12 +95,17 @@ const PROCESSED_ATTACHMENT: ProcessedAttachment = { }; describe('processDataMessage', () => { - const check = (message: Proto.IDataMessage) => + const check = ( + message: Partial> + ) => processDataMessage( - { - timestamp: Long.fromNumber(TIMESTAMP), - ...message, - }, + Proto.DataMessage.decode( + Proto.DataMessage.encode({ + ...EMPTY_DATA_MESSAGE, + timestamp: BigInt(TIMESTAMP), + ...message, + }) + ), TIMESTAMP, { _createName: () => 'random-path', @@ -94,7 +130,9 @@ describe('processDataMessage', () => { attachments: [ { ...UNPROCESSED_ATTACHMENT, - cdnId: new Long(0), + attachmentIdentifier: { + cdnId: 0n, + }, }, ], }); @@ -103,6 +141,7 @@ describe('processDataMessage', () => { { ...PROCESSED_ATTACHMENT, cdnId: undefined, + cdnKey: undefined, downloadPath: 'random-path', }, ]); @@ -154,7 +193,7 @@ describe('processDataMessage', () => { }); it('should throw on too many attachments', () => { - const attachments: Array = []; + const attachments: Array = []; for (let i = 0; i < ATTACHMENT_MAX + 1; i += 1) { attachments.push(UNPROCESSED_ATTACHMENT); } @@ -206,9 +245,12 @@ describe('processDataMessage', () => { it('should process quote, dropping second attachment', () => { const out = check({ quote: { - id: Long.fromNumber(1), + id: 1n, + authorAci: null, authorAciBinary: ACI_BINARY_1, text: 'text', + bodyRanges: null, + type: null, attachments: [ { contentType: 'image/jpeg', @@ -235,20 +277,31 @@ describe('processDataMessage', () => { thumbnail: PROCESSED_ATTACHMENT, }, ], - bodyRanges: undefined, + bodyRanges: [], type: 0, }); }); it('should process contact, dropping second contact', () => { + const EMPTY_CONTACT = { + $unknown: [], + number: [], + name: null, + email: [], + address: [], + organization: '', + }; const out = check({ contact: [ { + ...EMPTY_CONTACT, avatar: { avatar: UNPROCESSED_ATTACHMENT, + isProfile: false, }, }, { + ...EMPTY_CONTACT, avatar: { avatar: UNPROCESSED_ATTACHMENT, isProfile: true, @@ -259,7 +312,11 @@ describe('processDataMessage', () => { assert.deepStrictEqual(out.contact, [ { - avatar: { avatar: PROCESSED_ATTACHMENT, isProfile: false }, + ...EMPTY_CONTACT, + avatar: { + avatar: PROCESSED_ATTACHMENT, + isProfile: false, + }, }, ]); }); @@ -273,12 +330,14 @@ describe('processDataMessage', () => { image: UNPROCESSED_ATTACHMENT, title: 'Signal Private Messenger #1', url: 'https://signal.org', + date: null, }, { description: 'Say "hello" again', image: UNPROCESSED_ATTACHMENT, title: 'Signal Private Messenger #2', url: 'https://signal.org', + date: null, }, ], }); @@ -300,8 +359,10 @@ describe('processDataMessage', () => { check({ reaction: { emoji: '😎', + remove: null, + targetAuthorAci: null, targetAuthorAciBinary: ACI_BINARY_1, - targetSentTimestamp: Long.fromNumber(TIMESTAMP), + targetSentTimestamp: BigInt(TIMESTAMP), }, }).reaction, { @@ -317,8 +378,9 @@ describe('processDataMessage', () => { reaction: { emoji: '😎', remove: true, + targetAuthorAci: null, targetAuthorAciBinary: ACI_BINARY_1, - targetSentTimestamp: Long.fromNumber(TIMESTAMP), + targetSentTimestamp: BigInt(TIMESTAMP), }, }).reaction, { @@ -334,8 +396,11 @@ describe('processDataMessage', () => { const out = check({ preview: [ { - date: Long.fromNumber(TIMESTAMP), + date: BigInt(TIMESTAMP), image: UNPROCESSED_ATTACHMENT, + url: null, + title: null, + description: null, }, ], }); @@ -343,9 +408,9 @@ describe('processDataMessage', () => { assert.deepStrictEqual(out.preview, [ { date: TIMESTAMP, - description: undefined, - title: undefined, - url: undefined, + description: '', + title: '', + url: '', image: PROCESSED_ATTACHMENT, }, ]); diff --git a/ts/test-node/processSent_test.node.ts b/ts/test-node/processSent_test.node.ts new file mode 100644 index 0000000000..5d42c59bb3 --- /dev/null +++ b/ts/test-node/processSent_test.node.ts @@ -0,0 +1,58 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import lodash from 'lodash'; +import { generateAci } from '../types/ServiceId.std.js'; +import { signalservice as Proto } from '../protobuf/compiled.std.js'; + +import { processSent } from '../textsecure/processSyncMessage.node.js'; + +const { omit } = lodash; + +describe('processSent', () => { + const destinationServiceId = generateAci(); + + it('should normalize UUIDs in sent', () => { + const input = Proto.SyncMessage.Sent.decode( + Proto.SyncMessage.Sent.encode({ + destinationServiceId: destinationServiceId.toUpperCase(), + destinationServiceIdBinary: null, + + unidentifiedStatus: [ + { + destinationServiceId: destinationServiceId.toUpperCase(), + unidentified: null, + destinationServiceIdBinary: null, + destinationPniIdentityKey: null, + }, + ], + + destinationE164: null, + timestamp: null, + message: null, + expirationStartTimestamp: null, + isRecipientUpdate: null, + storyMessage: null, + storyMessageRecipients: null, + editMessage: null, + }) + ); + + const out = processSent(input); + + assert.deepStrictEqual(out, { + ...omit(input, 'destinationServiceIdBinary', '$unknown'), + destinationServiceId, + unidentifiedStatus: [ + { + $unknown: [], + destinationPniIdentityKey: undefined, + destinationServiceId, + unidentified: false, + }, + ], + storyMessageRecipients: [], + } as unknown); + }); +}); diff --git a/ts/test-node/processSyncMessage_test.node.ts b/ts/test-node/processSyncMessage_test.node.ts deleted file mode 100644 index 92b0506083..0000000000 --- a/ts/test-node/processSyncMessage_test.node.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { assert } from 'chai'; -import { generateAci } from '../types/ServiceId.std.js'; - -import { processSyncMessage } from '../textsecure/processSyncMessage.node.js'; - -describe('processSyncMessage', () => { - const destinationServiceId = generateAci(); - - it('should normalize UUIDs in sent', () => { - const out = processSyncMessage({ - sent: { - destinationServiceId: destinationServiceId.toUpperCase(), - - unidentifiedStatus: [ - { - destinationServiceId: destinationServiceId.toUpperCase(), - }, - ], - }, - }); - - assert.deepStrictEqual(out, { - sent: { - destinationServiceId, - - storyMessageRecipients: undefined, - unidentifiedStatus: [ - { - destinationServiceId, - }, - ], - }, - }); - }); -}); diff --git a/ts/test-node/sql/cleanDataForIpc_test.std.ts b/ts/test-node/sql/cleanDataForIpc_test.std.ts index f2e688850a..6872e663e6 100644 --- a/ts/test-node/sql/cleanDataForIpc_test.std.ts +++ b/ts/test-node/sql/cleanDataForIpc_test.std.ts @@ -182,27 +182,6 @@ describe('cleanDataForIpc', () => { ]); }); - it('calls `toNumber` when available', () => { - assert.deepEqual( - cleanDataForIpc([ - { - toNumber() { - return 5; - }, - }, - { - toNumber() { - return Symbol('bogus'); - }, - }, - ]), - { - cleaned: [5, undefined], - pathsChanged: ['root.1'], - } - ); - }); - it('deeply cleans objects with a `null` prototype', () => { const value = Object.assign(Object.create(null), { 'key 1': 'value', diff --git a/ts/test-node/sql/migration_1280_test.node.ts b/ts/test-node/sql/migration_1280_test.node.ts index 34b8ec75dd..00490194b3 100644 --- a/ts/test-node/sql/migration_1280_test.node.ts +++ b/ts/test-node/sql/migration_1280_test.node.ts @@ -13,6 +13,22 @@ import { getTableData, } from './helpers.node.js'; +const EMPTY_ENVELOPE: Proto.Envelope.Params = { + content: null, + type: null, + sourceServiceId: null, + sourceDevice: null, + destinationServiceId: null, + timestamp: null, + serverGuid: null, + serverTimestamp: null, + ephemeral: null, + urgent: null, + updatedPni: null, + story: null, + reportSpamToken: null, +}; + describe('SQL/updateToSchemaVersion1280', () => { let db: WritableDB; @@ -62,10 +78,11 @@ describe('SQL/updateToSchemaVersion1280', () => { attempts: 5, envelope: Buffer.from( Proto.Envelope.encode({ + ...EMPTY_ENVELOPE, destinationServiceId: THEIR_ACI, content: Buffer.from('encrypted1'), reportSpamToken: Buffer.from('token'), - }).finish() + }) ).toString('base64'), serverTimestamp: 6, serverGuid: 'guid1', @@ -81,9 +98,10 @@ describe('SQL/updateToSchemaVersion1280', () => { attempts: 5, envelope: Buffer.from( Proto.Envelope.encode({ + ...EMPTY_ENVELOPE, type: 3, content: Buffer.from('encrypted2'), - }).finish() + }) ).toString('base64'), serverTimestamp: 7, serverGuid: 'guid2', @@ -99,8 +117,9 @@ describe('SQL/updateToSchemaVersion1280', () => { attempts: 6, envelope: Buffer.from( Proto.Envelope.encode({ + ...EMPTY_ENVELOPE, content: Buffer.from('unused'), - }).finish() + }) ).toString('base64'), decrypted: 'CAFE', serverTimestamp: 8, diff --git a/ts/test-node/util/arePinnedConversationsEqual_test.node.ts b/ts/test-node/util/arePinnedConversationsEqual_test.node.ts index 785cc5b686..0c61b1a82c 100644 --- a/ts/test-node/util/arePinnedConversationsEqual_test.node.ts +++ b/ts/test-node/util/arePinnedConversationsEqual_test.node.ts @@ -5,30 +5,40 @@ import { assert } from 'chai'; import { arePinnedConversationsEqual } from '../../util/arePinnedConversationsEqual.node.js'; import { SignalService as Proto } from '../../protobuf/index.std.js'; -import PinnedConversation = Proto.AccountRecord.IPinnedConversation; +import PinnedConversation = Proto.AccountRecord.PinnedConversation.Params; describe('arePinnedConversationsEqual', () => { it('is equal if both have same values at same indices', () => { const localValue = [ { - contact: { - serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', - e164: '+13055551234', + identifier: { + contact: { + serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', + serviceIdBinary: null, + e164: '+13055551234', + }, }, }, { - groupMasterKey: new Uint8Array(32), + identifier: { + groupMasterKey: new Uint8Array(32), + }, }, ]; const remoteValue = [ { - contact: { - serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', - e164: '+13055551234', + identifier: { + contact: { + serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', + serviceIdBinary: null, + e164: '+13055551234', + }, }, }, { - groupMasterKey: new Uint8Array(32), + identifier: { + groupMasterKey: new Uint8Array(32), + }, }, ]; @@ -38,29 +48,41 @@ describe('arePinnedConversationsEqual', () => { it('is not equal if values are mixed', () => { const localValue = [ { - contact: { - serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', - e164: '+13055551234', + identifier: { + contact: { + serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', + serviceIdBinary: null, + e164: '+13055551234', + }, }, }, { - contact: { - serviceId: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25', - e164: '+17865554321', + identifier: { + contact: { + serviceId: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25', + serviceIdBinary: null, + e164: '+17865554321', + }, }, }, ]; const remoteValue = [ { - contact: { - serviceId: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25', - e164: '+17865554321', + identifier: { + contact: { + serviceId: 'f59a9fed-9e91-4bb4-a015-d49e58b47e25', + serviceIdBinary: null, + e164: '+17865554321', + }, }, }, { - contact: { - serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', - e164: '+13055551234', + identifier: { + contact: { + serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', + serviceIdBinary: null, + e164: '+13055551234', + }, }, }, ]; @@ -71,9 +93,12 @@ describe('arePinnedConversationsEqual', () => { it('is not equal if lengths are not same', () => { const localValue = [ { - contact: { - serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', - e164: '+13055551234', + identifier: { + contact: { + serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', + serviceIdBinary: null, + e164: '+13055551234', + }, }, }, ]; @@ -84,15 +109,20 @@ describe('arePinnedConversationsEqual', () => { it('is not equal if content does not match', () => { const localValue = [ { - contact: { - serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', - e164: '+13055551234', + identifier: { + contact: { + serviceId: '72313cde-2784-4a6f-a92a-abbe23763a60', + serviceIdBinary: null, + e164: '+13055551234', + }, }, }, ]; const remoteValue = [ { - groupMasterKey: new Uint8Array(32), + identifier: { + groupMasterKey: new Uint8Array(32), + }, }, ]; assert.isFalse(arePinnedConversationsEqual(localValue, remoteValue)); diff --git a/ts/test-node/util/callingMessageToProto_test.node.ts b/ts/test-node/util/callingMessageToProto_test.node.ts index b0671e98f1..99c61ac7fe 100644 --- a/ts/test-node/util/callingMessageToProto_test.node.ts +++ b/ts/test-node/util/callingMessageToProto_test.node.ts @@ -20,11 +20,11 @@ describe('callingMessageToProto', () => { describe('hangup field', () => { it('leaves the field unset if `hangup` is not provided', () => { const result = callingMessageToProto(new CallingMessage()); - assert.isUndefined(result.hangup); + assert.isNull(result.hangup); }); it('attaches the type if provided', () => { - const callId: CallId = { high: 0, low: 0, unsigned: false }; + const callId: CallId = 0n; const callingMessage = new CallingMessage(); callingMessage.hangup = new HangupMessage(callId, HangupType.Busy, 1); @@ -38,7 +38,7 @@ describe('callingMessageToProto', () => { describe('opaque field', () => { it('leaves the field unset if neither `opaque` nor urgency are provided', () => { const result = callingMessageToProto(new CallingMessage()); - assert.isUndefined(result.opaque); + assert.isNull(result.opaque); }); it('attaches opaque data', () => { diff --git a/ts/test-node/util/sessionTranslation_test.node.ts b/ts/test-node/util/sessionTranslation_test.node.ts index ba3652da23..cef0b2cea4 100644 --- a/ts/test-node/util/sessionTranslation_test.node.ts +++ b/ts/test-node/util/sessionTranslation_test.node.ts @@ -4,12 +4,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { assert } from 'chai'; -import Long from 'long'; import * as Bytes from '../../Bytes.std.js'; import type { LocalUserDataType } from '../../util/sessionTranslation.node.js'; import { sessionRecordToProtobuf } from '../../util/sessionTranslation.node.js'; +import { toNumber } from '../../util/toNumber.std.js'; + const getRecordCopy = (record: any): any => JSON.parse(JSON.stringify(record)); export const SESSION_V1_RECORD = { @@ -208,8 +209,8 @@ function protoToJSON(value: unknown): unknown { return value.map(protoToJSON); } - if (Long.isLong(value)) { - return value.toNumber(); + if (typeof value === 'bigint') { + return toNumber(value); } if (typeof value === 'object') { @@ -310,6 +311,7 @@ describe('sessionTranslation', () => { receiverChains: [ { senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + senderRatchetKeyPrivate: null, chainKey: { index: 6, key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', @@ -332,6 +334,8 @@ describe('sessionTranslation', () => { ], remoteRegistrationId: 4243, localRegistrationId: 3554, + needsRefresh: null, + pendingPreKey: null, aliceBaseKey: 'BVeHv5MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', }, previousSessions: [], @@ -370,6 +374,7 @@ describe('sessionTranslation', () => { receiverChains: [ { senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + senderRatchetKeyPrivate: null, chainKey: { index: 6, key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', @@ -391,8 +396,10 @@ describe('sessionTranslation', () => { }, { senderRatchetKey: 'BTpb20+IlnBkryDC2ecQT96Hd3t9/Qh3ljnA3509kxRa', + senderRatchetKeyPrivate: null, chainKey: { index: 2, + key: null, }, messageKeys: [ { @@ -405,8 +412,10 @@ describe('sessionTranslation', () => { }, { senderRatchetKey: 'Bd5nlMVr6YMBE5eh//tOWMgoOQakkneYri/YuVJpi0pJ', + senderRatchetKeyPrivate: null, chainKey: { index: 12, + key: null, }, messageKeys: [ { @@ -437,8 +446,10 @@ describe('sessionTranslation', () => { }, { senderRatchetKey: 'BYSxQO1OIs0ZSFN7JI/vF5Rb0VwaKjs+UAAfDkhOYfkp', + senderRatchetKeyPrivate: null, chainKey: { index: 6, + key: null, }, messageKeys: [ { @@ -469,22 +480,28 @@ describe('sessionTranslation', () => { }, { senderRatchetKey: 'BbXSFD/IoivRUvfnPzOaRLqDXEAwi4YEristfwiOj3IJ', + senderRatchetKeyPrivate: null, chainKey: { index: 3, + key: null, }, messageKeys: [], }, { senderRatchetKey: 'BRRAnr1NhizgCPPzmYV9qGBpvwCpSQH0Rx+UOtl78wUg', + senderRatchetKeyPrivate: null, chainKey: { index: 1, + key: null, }, messageKeys: [], }, { senderRatchetKey: 'BZvOKPA+kXiCg8TIP/52fu1reCDirC7wb5nyRGce3y4N', + senderRatchetKeyPrivate: null, chainKey: { index: 7, + key: null, }, messageKeys: [ { @@ -497,8 +514,10 @@ describe('sessionTranslation', () => { }, { senderRatchetKey: 'Ba9q9bHjMHfbUNDCU8+0O7cmEcIluq+wk3/d2f7q+ThG', + senderRatchetKeyPrivate: null, chainKey: { index: 4, + key: null, }, messageKeys: [ { @@ -523,28 +542,36 @@ describe('sessionTranslation', () => { }, { senderRatchetKey: 'BTwX5SmcUeBG7mwyOZ3YgxyXIN0ktzuEdWTfBUmPfGYG', + senderRatchetKeyPrivate: null, chainKey: { index: 2, + key: null, }, messageKeys: [], }, { senderRatchetKey: 'BV7ECvKbwKIAD61BXDYr0xr3JtckuKzR1Hw8cVPWGtlo', + senderRatchetKeyPrivate: null, chainKey: { index: 3, + key: null, }, messageKeys: [], }, { senderRatchetKey: 'BTC7rQqoykGR5Aaix7RkAhI5fSXufc6pVGN9OIC8EW5c', + senderRatchetKeyPrivate: null, chainKey: { index: 1, + key: null, }, messageKeys: [], }, ], remoteRegistrationId: 4243, localRegistrationId: 3554, + needsRefresh: null, + pendingPreKey: null, aliceBaseKey: 'BVeHv5MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', }, previousSessions: [], @@ -632,6 +659,7 @@ describe('sessionTranslation', () => { receiverChains: [ { senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + senderRatchetKeyPrivate: null, chainKey: { index: 6, key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', @@ -659,6 +687,7 @@ describe('sessionTranslation', () => { }, remoteRegistrationId: 4243, localRegistrationId: 3554, + needsRefresh: null, aliceBaseKey: 'BVeHv5MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', }, previousSessions: [], @@ -821,6 +850,7 @@ describe('sessionTranslation', () => { receiverChains: [ { senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + senderRatchetKeyPrivate: null, chainKey: { index: 6, key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', @@ -843,6 +873,8 @@ describe('sessionTranslation', () => { ], remoteRegistrationId: 4243, localRegistrationId: 3554, + needsRefresh: null, + pendingPreKey: null, aliceBaseKey: 'BVeHv5MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', }, previousSessions: [ @@ -865,6 +897,7 @@ describe('sessionTranslation', () => { receiverChains: [ { senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + senderRatchetKeyPrivate: null, chainKey: { index: 6, key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', @@ -887,6 +920,8 @@ describe('sessionTranslation', () => { ], remoteRegistrationId: 2312, localRegistrationId: 3554, + needsRefresh: null, + pendingPreKey: null, aliceBaseKey: 'BUFOv0MAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', }, { @@ -908,6 +943,7 @@ describe('sessionTranslation', () => { receiverChains: [ { senderRatchetKey: 'BQo3HG1UhWIh6A7NBxZtNGezBZH8nElZjOqNCBHPzlBz', + senderRatchetKeyPrivate: null, chainKey: { index: 6, key: 'Wnvy2TjYs0HdZFNahmsKw5cc9KEbW1nSwraDFmGwBDw=', @@ -930,6 +966,8 @@ describe('sessionTranslation', () => { ], remoteRegistrationId: 3432, localRegistrationId: 3554, + needsRefresh: null, + pendingPreKey: null, aliceBaseKey: 'BUJEv1oAbMgKeaoO/G1CMBdqhC7bo7Mtc4EWxI0oT19N', }, ], @@ -991,6 +1029,7 @@ describe('sessionTranslation', () => { aliceBaseKey: 'BTU+PWVWuRnbiW6+ja+XI9+2Xz0TLk7uGqUlhS1d+V8K', localIdentityPublic: 'Baioqfzc/5JD6b+GNqapPouf6eHK7xr9ynLJHnvl+444', localRegistrationId: 3554, + needsRefresh: null, pendingPreKey: { baseKey: 'BTU+PWVWuRnbiW6+ja+XI9+2Xz0TLk7uGqUlhS1d+V8K', preKeyId: 386, diff --git a/ts/test-node/util/timestampLongUtils_test.std.ts b/ts/test-node/util/timestampLongUtils_test.std.ts index d7f7e57509..d42fed8bce 100644 --- a/ts/test-node/util/timestampLongUtils_test.std.ts +++ b/ts/test-node/util/timestampLongUtils_test.std.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; -import Long from 'long'; import { getSafeLongFromTimestamp, @@ -12,34 +11,38 @@ import { getCheckedTimestampOrUndefinedFromLong, } from '../../util/timestampLongUtils.std.js'; import { MAX_SAFE_DATE } from '../../util/timestamp.std.js'; +import { MAX_VALUE } from '../../util/long.std.js'; + +import { toNumber } from '../../util/toNumber.std.js'; describe('getSafeLongFromTimestamp', () => { it('returns zero when passed undefined', () => { - assert(getSafeLongFromTimestamp(undefined).isZero()); + assert.strictEqual(getSafeLongFromTimestamp(undefined), 0n); }); it('returns the number as a Long when passed a "normal" number', () => { - assert(getSafeLongFromTimestamp(0).isZero()); + assert.strictEqual(getSafeLongFromTimestamp(0), 0n); assert.strictEqual(getSafeLongFromTimestamp(123).toString(), '123'); assert.strictEqual(getSafeLongFromTimestamp(-456).toString(), '-456'); }); it('returns MAX_SAFE_DATE when passed Infinity', () => { assert.strictEqual( - getSafeLongFromTimestamp(Infinity).toNumber(), + toNumber(getSafeLongFromTimestamp(Infinity)), MAX_SAFE_DATE ); }); - it('returns Long.MAX_VALUE when passed Infinity and overriden', () => { - assert( - getSafeLongFromTimestamp(Infinity, Long.MAX_VALUE).equals(Long.MAX_VALUE) + it('returns MAX_VALUE when passed Infinity and overriden', () => { + assert.strictEqual( + getSafeLongFromTimestamp(Infinity, MAX_VALUE), + MAX_VALUE ); }); it("returns MAX_SAFE_DATE when passed very large numbers, outside of JavaScript's safely representable range", () => { assert.strictEqual( - getSafeLongFromTimestamp(Number.MAX_VALUE).toNumber(), + toNumber(getSafeLongFromTimestamp(Number.MAX_VALUE)), MAX_SAFE_DATE ); }); @@ -47,19 +50,19 @@ describe('getSafeLongFromTimestamp', () => { describe('getTimestampFromLong', () => { it('returns zero when passed negative Long', () => { - assert.equal(getTimestampFromLong(Long.fromNumber(-1)), 0); + assert.equal(getTimestampFromLong(BigInt(-1)), 0); }); it('returns zero when passed 0 Long', () => { - assert.equal(getTimestampFromLong(Long.fromNumber(0)), 0); + assert.equal(getTimestampFromLong(0n), 0); }); - it('returns MAX_SAFE_DATE when passed Long.MAX_VALUE', () => { - assert.equal(getTimestampFromLong(Long.MAX_VALUE), MAX_SAFE_DATE); + it('returns MAX_SAFE_DATE when passed MAX_VALUE', () => { + assert.equal(getTimestampFromLong(MAX_VALUE), MAX_SAFE_DATE); }); it('returns a normal number', () => { - assert.equal(getTimestampFromLong(Long.fromNumber(16)), 16); + assert.equal(getTimestampFromLong(16n), 16); }); it('returns 0 for null value', () => { @@ -73,35 +76,29 @@ describe('getCheckedTimestampFromLong', () => { }); it('throws on negative Long', () => { - assert.throws(() => getCheckedTimestampFromLong(Long.fromNumber(-1))); + assert.throws(() => getCheckedTimestampFromLong(BigInt(-1))); }); - it('throws on Long.MAX_VALUE', () => { - assert.throws(() => getCheckedTimestampFromLong(Long.MAX_VALUE)); + it('throws on MAX_VALUE', () => { + assert.throws(() => getCheckedTimestampFromLong(MAX_VALUE)); }); it('does not throw otherwise', () => { - assert.equal(getCheckedTimestampFromLong(Long.fromNumber(16)), 16); + assert.equal(getCheckedTimestampFromLong(16n), 16); }); }); describe('getTimestampOrUndefinedFromLong', () => { it('returns undefined when passed 0 Long', () => { - assert.equal( - getTimestampOrUndefinedFromLong(Long.fromNumber(0)), - undefined - ); + assert.equal(getTimestampOrUndefinedFromLong(0n), undefined); }); - it('returns MAX_SAFE_DATE when passed Long.MAX_VALUE', () => { - assert.equal( - getTimestampOrUndefinedFromLong(Long.MAX_VALUE), - MAX_SAFE_DATE - ); + it('returns MAX_SAFE_DATE when passed MAX_VALUE', () => { + assert.equal(getTimestampOrUndefinedFromLong(MAX_VALUE), MAX_SAFE_DATE); }); it('returns a normal number', () => { - assert.equal(getTimestampOrUndefinedFromLong(Long.fromNumber(16)), 16); + assert.equal(getTimestampOrUndefinedFromLong(16n), 16); }); it('returns undefined for null value', () => { @@ -111,9 +108,7 @@ describe('getTimestampOrUndefinedFromLong', () => { describe('getCheckedTimestampOrUndefinedFromLong', () => { it('throws on negative Long', () => { - assert.throws(() => - getCheckedTimestampOrUndefinedFromLong(Long.fromNumber(-1)) - ); + assert.throws(() => getCheckedTimestampOrUndefinedFromLong(-1n)); }); it('returns undefined on absent Long', () => { @@ -121,13 +116,10 @@ describe('getCheckedTimestampOrUndefinedFromLong', () => { }); it('returns undefined on zero Long', () => { - assert.equal(getCheckedTimestampOrUndefinedFromLong(Long.ZERO), undefined); + assert.equal(getCheckedTimestampOrUndefinedFromLong(0n), undefined); }); it('returns a normal number', () => { - assert.equal( - getCheckedTimestampOrUndefinedFromLong(Long.fromNumber(16)), - 16 - ); + assert.equal(getCheckedTimestampOrUndefinedFromLong(16n), 16); }); }); diff --git a/ts/textsecure/AccountManager.preload.ts b/ts/textsecure/AccountManager.preload.ts index a505e4ca7d..0bafe678ad 100644 --- a/ts/textsecure/AccountManager.preload.ts +++ b/ts/textsecure/AccountManager.preload.ts @@ -280,12 +280,11 @@ export default class AccountManager extends EventTarget { } const encrypted = encryptDeviceName(name, identityKey.publicKey); - const proto = new Proto.DeviceName(); - proto.ephemeralPublic = encrypted.ephemeralPublic.serialize(); - proto.syntheticIv = encrypted.syntheticIv; - proto.ciphertext = encrypted.ciphertext; - - const bytes = Proto.DeviceName.encode(proto).finish(); + const bytes = Proto.DeviceName.encode({ + ephemeralPublic: encrypted.ephemeralPublic.serialize(), + syntheticIv: encrypted.syntheticIv, + ciphertext: encrypted.ciphertext, + }); return Bytes.toBase64(bytes); } diff --git a/ts/textsecure/ContactsParser.preload.ts b/ts/textsecure/ContactsParser.preload.ts index 9c6247d7a5..3b95baa986 100644 --- a/ts/textsecure/ContactsParser.preload.ts +++ b/ts/textsecure/ContactsParser.preload.ts @@ -14,12 +14,11 @@ import type { ContactAvatarType } from '../types/Avatar.std.js'; import type { AttachmentType } from '../types/Attachment.std.js'; import type { AciString } from '../types/ServiceId.std.js'; import { computeHash } from '../Crypto.node.js'; -import { dropNull } from '../util/dropNull.std.js'; import { fromAciUuidBytesOrString } from '../util/ServiceId.node.js'; import * as Bytes from '../Bytes.std.js'; import { decryptAttachmentV2ToSink } from '../AttachmentCrypto.node.js'; -import Avatar = Proto.ContactDetails.IAvatar; +import Avatar = Proto.ContactDetails.Avatar.Params; import { stringToMIMEType } from '../types/MIME.std.js'; const log = createLogger('ContactsParser'); @@ -32,7 +31,7 @@ type OptionalFields = { type MessageWithAvatar = Omit< Message, - 'avatar' | 'toJSON' | 'aci' | 'aciBinary' + 'avatar' | 'toJSON' | 'aci' | 'aciBinary' | 'expireTimer' > & { aci: AciString; avatar?: ContactAvatarType; @@ -41,7 +40,8 @@ type MessageWithAvatar = Omit< number?: string | undefined; }; -export type ContactDetailsWithAvatar = MessageWithAvatar; +export type ContactDetailsWithAvatar = + MessageWithAvatar; export async function parseContactsV2( attachment: AttachmentType @@ -105,7 +105,11 @@ async function prepareContact( ? DurationInSeconds.fromSeconds(proto.expireTimer) : undefined; - const aci = fromAciUuidBytesOrString(aciBinary, rawAci, 'ContactBuffer.aci'); + const aci = fromAciUuidBytesOrString( + aciBinary, + rawAci ?? '', + 'ContactBuffer.aci' + ); if ((Bytes.isNotEmpty(aciBinary) || rawAci) && aci == null) { log.warn('ParseContactsTransform: Dropping contact with invalid aci'); @@ -132,9 +136,8 @@ async function prepareContact( return { ...proto, expireTimer, - expireTimerVersion: proto.expireTimerVersion ?? null, aci, avatar, - number: dropNull(proto.number), + number: proto.number ?? '', }; } diff --git a/ts/textsecure/MessageReceiver.preload.ts b/ts/textsecure/MessageReceiver.preload.ts index bdb380b67b..2435d6b8e2 100644 --- a/ts/textsecure/MessageReceiver.preload.ts +++ b/ts/textsecure/MessageReceiver.preload.ts @@ -50,6 +50,7 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary.std.js' import { Zone } from '../util/Zone.std.js'; import * as durations from '../util/durations/index.std.js'; import { DurationInSeconds } from '../util/durations/index.std.js'; +import { isKnownProtoEnumMember } from '../util/isKnownProtoEnumMember.std.js'; import { Address } from '../types/Address.std.js'; import { QualifiedAddress } from '../types/QualifiedAddress.std.js'; import { normalizeStoryDistributionId } from '../types/StoryDistributionId.std.js'; @@ -78,10 +79,11 @@ import createTaskWithTimeout from './TaskWithTimeout.std.js'; import { processAttachment, processDataMessage, + processBodyRange, processGroupV2Context, processPreview, } from './processDataMessage.preload.js'; -import { processSyncMessage } from './processSyncMessage.node.js'; +import { processSent } from './processSyncMessage.node.js'; import type { EventHandler } from './EventTarget.std.js'; import EventTarget from './EventTarget.std.js'; import type { IncomingWebSocketRequest } from './WebsocketResources.preload.js'; @@ -97,7 +99,6 @@ import type { ProcessedEnvelope, ProcessedPreview, ProcessedSent, - ProcessedSyncMessage, UnprocessedType, } from './Types.d.ts'; import type { @@ -173,6 +174,8 @@ import { MessageRequestResponseSource, } from '../types/MessageRequestResponseEvent.std.js'; +import { toNumber } from '../util/toNumber.std.js'; + const { isBoolean, isNumber, isString, noop, omit } = lodash; const log = createLogger('MessageReceiver'); @@ -412,12 +415,11 @@ export default class MessageReceiver const plaintext = request.body; const decoded = Proto.Envelope.decode(plaintext); - const serverTimestamp = decoded.serverTimestamp?.toNumber() ?? 0; + const serverTimestamp = toNumber(decoded.serverTimestamp) ?? 0; const ourAci = this.#storage.user.getCheckedAci(); const { content } = decoded; - strictAssert(content != null, 'Content is required for envelopes'); const envelope: ProcessedEnvelope = { // Make non-private envelope IDs dashless so they don't get redacted @@ -432,7 +434,9 @@ export default class MessageReceiver ), // Proto.Envelope fields - type: decoded.type ?? Proto.Envelope.Type.UNKNOWN, + type: isKnownProtoEnumMember(Proto.Envelope.Type, decoded.type) + ? decoded.type + : Proto.Envelope.Type.UNKNOWN, source: undefined, sourceServiceId: fromServiceIdBinaryOrString( decoded.sourceServiceIdBinary, @@ -451,8 +455,8 @@ export default class MessageReceiver decoded.updatedPni, 'MessageReceiver.handleRequest.updatedPni' ), - timestamp: decoded.clientTimestamp?.toNumber() ?? 0, - content, + timestamp: toNumber(decoded.clientTimestamp) ?? 0, + content: content ?? new Uint8Array(0), serverGuid: (Bytes.isNotEmpty(decoded.serverGuidBinary) ? bytesToUuid(decoded.serverGuidBinary) @@ -1473,7 +1477,10 @@ export default class MessageReceiver let inProgressMessageType = ''; try { const content = Proto.Content.decode(plaintext); - if (!wasEncrypted && Bytes.isEmpty(content.decryptionErrorMessage)) { + if ( + !wasEncrypted && + Bytes.isEmpty(content.content?.decryptionErrorMessage) + ) { log.warn( `${logId}: dropping plaintext envelope without decryption error message` ); @@ -1504,8 +1511,8 @@ export default class MessageReceiver } isGroupV2 = - Boolean(content.dataMessage?.groupV2) || - Boolean(content.storyMessage?.group); + Boolean(content.content?.dataMessage?.groupV2) || + Boolean(content.content?.storyMessage?.group); if ( wasEncrypted && @@ -1520,12 +1527,12 @@ export default class MessageReceiver ); } - const isStoryReply = Boolean(content.dataMessage?.storyContext); + const isStoryReply = Boolean(content.content?.dataMessage?.storyContext); const isGroupStoryReply = Boolean( - isStoryReply && content.dataMessage?.groupV2 + isStoryReply && content.content?.dataMessage?.groupV2 ); - const isStory = Boolean(content.storyMessage); - const isDeleteForEveryone = Boolean(content.dataMessage?.delete); + const isStory = Boolean(content.content?.storyMessage); + const isDeleteForEveryone = Boolean(content.content?.dataMessage?.delete); if ( envelope.story && @@ -1581,12 +1588,12 @@ export default class MessageReceiver // Some sync messages have to be fully processed in the middle of // decryption queue since subsequent envelopes use their key material. - const { syncMessage } = content; - if (wasEncrypted && syncMessage?.pniChangeNumber) { + const syncMessage = content.content?.syncMessage; + if (wasEncrypted && syncMessage?.content?.pniChangeNumber) { inProgressMessageType = 'pni change number'; await this.#handlePNIChangeNumber( envelope, - syncMessage.pniChangeNumber + syncMessage.content.pniChangeNumber ); this.#removeFromCache(envelope); return { plaintext: undefined, envelope }; @@ -2111,9 +2118,9 @@ export default class MessageReceiver const ev = new SentEvent( { envelopeId: envelope.id, - destinationE164: dropNull(destinationE164), + destinationE164: destinationE164 ?? '', destinationServiceId, - timestamp: timestamp.toNumber(), + timestamp: toNumber(timestamp), serverTimestamp: envelope.serverTimestamp, device: envelope.sourceDevice, unidentifiedStatus, @@ -2121,7 +2128,7 @@ export default class MessageReceiver isRecipientUpdate: Boolean(isRecipientUpdate), receivedAtCounter: envelope.receivedAtCounter, receivedAtDate: envelope.receivedAtDate, - expirationStartTimestamp: expirationStartTimestamp?.toNumber(), + expirationStartTimestamp: toNumber(expirationStartTimestamp) ?? 0, }, this.#removeFromCache.bind(this, envelope) ); @@ -2130,7 +2137,7 @@ export default class MessageReceiver async #handleStoryMessage( envelope: UnsealedEnvelope, - msg: Proto.IStoryMessage, + msg: Proto.StoryMessage, sentMessage?: ProcessedSent ): Promise { const envelopeId = getEnvelopeId(envelope); @@ -2155,16 +2162,17 @@ export default class MessageReceiver const attachments: Array = []; let preview: ReadonlyArray | undefined; - if (msg.fileAttachment) { - const attachment = processAttachment(msg.fileAttachment); + if (msg.attachment?.fileAttachment != null) { + const attachment = processAttachment(msg.attachment.fileAttachment); attachments.push(attachment); } - if (msg.textAttachment) { + if (msg.attachment?.textAttachment != null) { // If a text attachment has a link preview we remove it from the // textAttachment data structure and instead process the preview and add // it as a "preview" property for the message attributes. - const { text, preview: unprocessedPreview } = msg.textAttachment; + const { text, preview: unprocessedPreview } = + msg.attachment.textAttachment; if (unprocessedPreview) { preview = processPreview([unprocessedPreview]); } else if (!text) { @@ -2174,10 +2182,18 @@ export default class MessageReceiver attachments.push({ size: text?.length ?? 0, contentType: TEXT_ATTACHMENT, - textAttachment: omit(msg.textAttachment, 'preview'), + textAttachment: { + ...omit(msg.attachment.textAttachment, 'preview'), + textStyle: isKnownProtoEnumMember( + Proto.TextAttachment.Style, + msg.attachment.textAttachment.textStyle + ) + ? msg.attachment.textAttachment.textStyle + : 0, + }, blurHash: generateBlurHash( - (msg.textAttachment.color || - msg.textAttachment.gradient?.startColor) ?? + (msg.attachment.textAttachment.background?.color || + msg.attachment.textAttachment.background?.gradient?.startColor) ?? undefined ), }); @@ -2204,7 +2220,9 @@ export default class MessageReceiver const message: ProcessedDataMessage = { attachments, - bodyRanges: filterAndClean(msg.bodyRanges), + bodyRanges: filterAndClean( + msg.bodyRanges.map(processBodyRange).filter(isNotNil) + ), preview, canReplyToStory: Boolean(msg.allowsReplies), expireTimer: DurationInSeconds.DAY, @@ -2238,6 +2256,8 @@ export default class MessageReceiver return { destinationServiceId, isAllowedToReplyToStory: Boolean(isAllowedToReply), + destinationPniIdentityKey: undefined, + unidentified: false, }; }) .filter(isNotNil), @@ -2298,6 +2318,7 @@ export default class MessageReceiver destinationServiceId, isAllowedToReplyToStory: isAllowedToReply.has(destinationServiceId), + unidentified: false, }) ), message, @@ -2341,7 +2362,7 @@ export default class MessageReceiver async #handleEditMessage( envelope: UnsealedEnvelope, - msg: Proto.IEditMessage + msg: Proto.EditMessage ): Promise { const logId = `handleEditMessage(${getEnvelopeId(envelope)})`; log.info(logId); @@ -2391,7 +2412,7 @@ export default class MessageReceiver ), message: { ...message, - editedMessageTimestamp: msg.targetSentTimestamp.toNumber(), + editedMessageTimestamp: toNumber(msg.targetSentTimestamp), }, receivedAtCounter: envelope.receivedAtCounter, receivedAtDate: envelope.receivedAtDate, @@ -2403,7 +2424,7 @@ export default class MessageReceiver async #handleDataMessage( envelope: UnsealedEnvelope, - msg: Proto.IDataMessage + msg: Proto.DataMessage ): Promise { const logId = `handleDataMessage/${getEnvelopeId(envelope)}`; log.info(logId); @@ -2577,47 +2598,50 @@ export default class MessageReceiver const envelope = await this.#maybeUpdateTimestamp(incomingEnvelope); if ( - content.decryptionErrorMessage && - Bytes.isNotEmpty(content.decryptionErrorMessage) + content.content?.decryptionErrorMessage && + Bytes.isNotEmpty(content.content.decryptionErrorMessage) ) { - this.#handleDecryptionError(envelope, content.decryptionErrorMessage); - return; - } - if (content.syncMessage) { - await this.#handleSyncMessage( + this.#handleDecryptionError( envelope, - processSyncMessage(content.syncMessage) + content.content.decryptionErrorMessage ); return; } - if (content.dataMessage) { - await this.#handleDataMessage(envelope, content.dataMessage); + if (content.content?.syncMessage) { + await this.#handleSyncMessage(envelope, content.content.syncMessage); return; } - if (content.nullMessage) { + if (content.content?.dataMessage) { + await this.#handleDataMessage(envelope, content.content.dataMessage); + return; + } + if (content.content?.nullMessage) { this.#handleNullMessage(envelope); return; } - if (content.callMessage) { - await this.#handleCallingMessage(envelope, content.callMessage); + if (content.content?.callMessage) { + await this.#handleCallingMessage(envelope, content.content.callMessage); return; } - if (content.receiptMessage) { - await this.#handleReceiptMessage(envelope, content.receiptMessage); + if (content.content?.receiptMessage) { + await this.#handleReceiptMessage( + envelope, + content.content.receiptMessage + ); return; } - if (content.typingMessage) { - this.#handleTypingMessage(envelope, content.typingMessage); + if (content.content?.typingMessage) { + this.#handleTypingMessage(envelope, content.content.typingMessage); return; } - if (content.storyMessage) { - await this.#handleStoryMessage(envelope, content.storyMessage); + if (content.content?.storyMessage) { + await this.#handleStoryMessage(envelope, content.content.storyMessage); return; } - if (content.editMessage) { - await this.#handleEditMessage(envelope, content.editMessage); + if (content.content?.editMessage) { + await this.#handleEditMessage(envelope, content.content.editMessage); return; } @@ -2710,7 +2734,7 @@ export default class MessageReceiver async #handlePniSignatureMessage( envelope: UnsealedEnvelope, - pniSignatureMessage: Proto.IPniSignatureMessage + pniSignatureMessage: Proto.PniSignatureMessage ): Promise { const envelopeId = getEnvelopeId(envelope); const logId = `handlePniSignatureMessage/${envelopeId}`; @@ -2754,7 +2778,7 @@ export default class MessageReceiver async #handleCallingMessage( envelope: UnsealedEnvelope, - callingMessage: Proto.ICallMessage + callingMessage: Proto.CallMessage ): Promise { logUnexpectedUrgentValue(envelope, 'callingMessage'); @@ -2778,7 +2802,7 @@ export default class MessageReceiver async #handleReceiptMessage( envelope: UnsealedEnvelope, - receiptMessage: Proto.IReceiptMessage + receiptMessage: Proto.ReceiptMessage ): Promise { strictAssert(receiptMessage.timestamp, 'Receipt message without timestamp'); @@ -2808,7 +2832,7 @@ export default class MessageReceiver const logId = getEnvelopeId(envelope); const receipts = receiptMessage.timestamp.map(rawTimestamp => ({ - timestamp: rawTimestamp?.toNumber(), + timestamp: toNumber(rawTimestamp), source: envelope.source, sourceServiceId: envelope.sourceServiceId, sourceDevice: envelope.sourceDevice, @@ -2828,7 +2852,7 @@ export default class MessageReceiver #handleTypingMessage( envelope: UnsealedEnvelope, - typingMessage: Proto.ITypingMessage + typingMessage: Proto.TypingMessage ): void { this.#removeFromCache(envelope); @@ -2836,7 +2860,7 @@ export default class MessageReceiver if (envelope.timestamp && typingMessage.timestamp) { const envelopeTimestamp = envelope.timestamp; - const typingTimestamp = typingMessage.timestamp?.toNumber(); + const typingTimestamp = toNumber(typingMessage.timestamp); if (typingTimestamp !== envelopeTimestamp) { log.warn( @@ -2874,7 +2898,7 @@ export default class MessageReceiver typing: { groupV2Id: groupV2IdString, typingMessage, - timestamp: timestamp?.toNumber() ?? Date.now(), + timestamp: toNumber(timestamp) ?? Date.now(), started: action === Proto.TypingMessage.Action.STARTED, stopped: action === Proto.TypingMessage.Action.STOPPED, }, @@ -2891,7 +2915,7 @@ export default class MessageReceiver } #isInvalidGroupData( - message: Proto.IDataMessage, + message: Proto.DataMessage, envelope: ProcessedEnvelope ): boolean { const { groupV2 } = message; @@ -2920,7 +2944,7 @@ export default class MessageReceiver return undefined; } - #getGroupId(message: Proto.IDataMessage): string | undefined { + #getGroupId(message: Proto.DataMessage): string | undefined { if (message.groupV2) { strictAssert(message.groupV2.masterKey, 'Missing groupV2.masterKey'); const { id } = deriveGroupFields(message.groupV2.masterKey); @@ -2939,7 +2963,7 @@ export default class MessageReceiver async #handleSyncMessage( envelope: UnsealedEnvelope, - syncMessage: ProcessedSyncMessage + syncMessage: Proto.SyncMessage ): Promise { const ourNumber = this.#storage.user.getNumber(); const ourAci = this.#storage.user.getCheckedAci(); @@ -2956,8 +2980,8 @@ export default class MessageReceiver if (envelope.sourceDevice == ourDeviceId) { throw new Error('Received sync message from our own device'); } - if (syncMessage.sent) { - const sentMessage = syncMessage.sent; + if (syncMessage.content?.sent) { + const sentMessage = processSent(syncMessage.content.sent); if (sentMessage.editMessage) { return this.#handleSentEditMessage(envelope, sentMessage); @@ -3016,23 +3040,23 @@ export default class MessageReceiver log.info( 'sent message to', this.#getDestination(sentMessage), - sentMessage.timestamp?.toNumber(), + toNumber(sentMessage.timestamp), 'from', getEnvelopeId(envelope) ); return this.#handleSentMessage(envelope, sentMessage); } - if (syncMessage.contacts) { + if (syncMessage.content?.contacts) { // Note: this method will download attachment and thus might block // message processing, but we would like to fully process contact sync // before moving on since it updates conversation state. - return this.#handleContacts(envelope, syncMessage.contacts); + return this.#handleContacts(envelope, syncMessage.content.contacts); } - if (syncMessage.blocked) { - return this.#handleBlocked(envelope, syncMessage.blocked); + if (syncMessage.content?.blocked) { + return this.#handleBlocked(envelope, syncMessage.content.blocked); } - if (syncMessage.request) { + if (syncMessage.content?.request) { log.info('Got SyncMessage Request'); this.#removeFromCache(envelope); return; @@ -3040,13 +3064,16 @@ export default class MessageReceiver if (syncMessage.read && syncMessage.read.length) { return this.#handleRead(envelope, syncMessage.read); } - if (syncMessage.verified) { + if (syncMessage.content?.verified) { log.info('Got verified sync message, dropping'); this.#removeFromCache(envelope); return; } - if (syncMessage.configuration) { - return this.#handleConfiguration(envelope, syncMessage.configuration); + if (syncMessage.content?.configuration) { + return this.#handleConfiguration( + envelope, + syncMessage.content.configuration + ); } if ( syncMessage.stickerPackOperation && @@ -3057,46 +3084,58 @@ export default class MessageReceiver syncMessage.stickerPackOperation ); } - if (syncMessage.viewOnceOpen) { - return this.#handleViewOnceOpen(envelope, syncMessage.viewOnceOpen); - } - if (syncMessage.messageRequestResponse) { - return this.#handleMessageRequestResponse( + if (syncMessage.content?.viewOnceOpen) { + return this.#handleViewOnceOpen( envelope, - syncMessage.messageRequestResponse + syncMessage.content.viewOnceOpen ); } - if (syncMessage.fetchLatest) { - return this.#handleFetchLatest(envelope, syncMessage.fetchLatest); + if (syncMessage.content?.messageRequestResponse) { + return this.#handleMessageRequestResponse( + envelope, + syncMessage.content.messageRequestResponse + ); } - if (syncMessage.keys) { - return this.#handleKeys(envelope, syncMessage.keys); + if (syncMessage.content?.fetchLatest) { + return this.#handleFetchLatest(envelope, syncMessage.content.fetchLatest); + } + if (syncMessage.content?.keys) { + return this.#handleKeys(envelope, syncMessage.content.keys); } if (syncMessage.viewed && syncMessage.viewed.length) { return this.#handleViewed(envelope, syncMessage.viewed); } - if (syncMessage.callEvent) { - return this.#handleCallEvent(envelope, syncMessage.callEvent); + if (syncMessage.content?.callEvent) { + return this.#handleCallEvent(envelope, syncMessage.content.callEvent); } - if (syncMessage.callLinkUpdate) { - return this.#handleCallLinkUpdate(envelope, syncMessage.callLinkUpdate); - } - if (syncMessage.callLogEvent) { - return this.#handleCallLogEvent(envelope, syncMessage.callLogEvent); - } - if (syncMessage.deleteForMe) { - return this.#handleDeleteForMeSync(envelope, syncMessage.deleteForMe); - } - if (syncMessage.deviceNameChange) { - return this.#handleDeviceNameChangeSync( + if (syncMessage.content?.callLinkUpdate) { + return this.#handleCallLinkUpdate( envelope, - syncMessage.deviceNameChange + syncMessage.content.callLinkUpdate ); } - if (syncMessage.attachmentBackfillResponse) { + if (syncMessage.content?.callLogEvent) { + return this.#handleCallLogEvent( + envelope, + syncMessage.content.callLogEvent + ); + } + if (syncMessage.content?.deleteForMe) { + return this.#handleDeleteForMeSync( + envelope, + syncMessage.content.deleteForMe + ); + } + if (syncMessage.content?.deviceNameChange) { + return this.#handleDeviceNameChangeSync( + envelope, + syncMessage.content.deviceNameChange + ); + } + if (syncMessage.content?.attachmentBackfillResponse) { return this.#handleAttachmentBackfillResponse( envelope, - syncMessage.attachmentBackfillResponse + syncMessage.content.attachmentBackfillResponse ); } @@ -3148,7 +3187,7 @@ export default class MessageReceiver const ev = new SentEvent( { envelopeId: envelope.id, - destinationE164: dropNull(destinationE164), + destinationE164: destinationE164 ?? '', destinationServiceId, timestamp: envelope.timestamp, serverTimestamp: envelope.serverTimestamp, @@ -3156,12 +3195,12 @@ export default class MessageReceiver unidentifiedStatus, message: { ...message, - editedMessageTimestamp: editMessage.targetSentTimestamp.toNumber(), + editedMessageTimestamp: toNumber(editMessage.targetSentTimestamp), }, isRecipientUpdate: Boolean(isRecipientUpdate), receivedAtCounter: envelope.receivedAtCounter, receivedAtDate: envelope.receivedAtDate, - expirationStartTimestamp: expirationStartTimestamp?.toNumber(), + expirationStartTimestamp: toNumber(expirationStartTimestamp) ?? 0, }, this.#removeFromCache.bind(this, envelope) ); @@ -3170,7 +3209,7 @@ export default class MessageReceiver async #handleConfiguration( envelope: ProcessedEnvelope, - configuration: Proto.SyncMessage.IConfiguration + configuration: Proto.SyncMessage.Configuration ): Promise { const logId = getEnvelopeId(envelope); log.info('got configuration sync message', logId); @@ -3186,7 +3225,7 @@ export default class MessageReceiver async #handleViewOnceOpen( envelope: ProcessedEnvelope, - sync: Proto.SyncMessage.IViewOnceOpen + sync: Proto.SyncMessage.ViewOnceOpen ): Promise { const logId = getEnvelopeId(envelope); log.info('got view once open sync message', logId); @@ -3200,7 +3239,7 @@ export default class MessageReceiver sync.senderAci, 'handleViewOnceOpen.senderUuid' ), - timestamp: sync.timestamp?.toNumber(), + timestamp: toNumber(sync.timestamp) ?? 0, envelopeTimestamp: envelope.timestamp, }, this.#removeFromCache.bind(this, envelope) @@ -3211,7 +3250,7 @@ export default class MessageReceiver async #handleMessageRequestResponse( envelope: ProcessedEnvelope, - sync: Proto.SyncMessage.IMessageRequestResponse + sync: Proto.SyncMessage.MessageRequestResponse ): Promise { const logId = getEnvelopeId(envelope); log.info('got message request response sync message', logId); @@ -3239,7 +3278,12 @@ export default class MessageReceiver sync.threadAci, 'handleMessageRequestResponse.threadUuid' ), - messageRequestResponseType: sync.type, + messageRequestResponseType: isKnownProtoEnumMember( + Proto.SyncMessage.MessageRequestResponse.Type, + sync.type + ) + ? sync.type + : Proto.SyncMessage.MessageRequestResponse.Type.UNKNOWN, groupV2Id: groupV2IdString, receivedAtCounter: envelope.receivedAtCounter, receivedAtMs: envelope.receivedAtDate, @@ -3253,7 +3297,7 @@ export default class MessageReceiver async #handleFetchLatest( envelope: ProcessedEnvelope, - sync: Proto.SyncMessage.IFetchLatest + sync: Proto.SyncMessage.FetchLatest ): Promise { const logId = getEnvelopeId(envelope); log.info('got fetch latest sync message', logId); @@ -3270,7 +3314,7 @@ export default class MessageReceiver async #handleKeys( envelope: ProcessedEnvelope, - sync: Proto.SyncMessage.IKeys + sync: Proto.SyncMessage.Keys ): Promise { const logId = getEnvelopeId(envelope); log.info('got keys sync message', logId); @@ -3300,7 +3344,7 @@ export default class MessageReceiver lastResortKyberPreKey, registrationId, newE164, - }: Proto.SyncMessage.IPniChangeNumber + }: Proto.SyncMessage.PniChangeNumber ): Promise { const ourAci = this.#storage.user.getCheckedAci(); @@ -3345,7 +3389,7 @@ export default class MessageReceiver async #handleStickerPackOperation( envelope: ProcessedEnvelope, - operations: Array + operations: Array ): Promise { const ENUM = Proto.SyncMessage.StickerPackOperation.Type; const logId = getEnvelopeId(envelope); @@ -3369,7 +3413,7 @@ export default class MessageReceiver async #handleRead( envelope: ProcessedEnvelope, - read: Array + read: Array ): Promise { const logId = getEnvelopeId(envelope); log.info('handleRead', logId); @@ -3382,7 +3426,7 @@ export default class MessageReceiver return { envelopeId: envelope.id, envelopeTimestamp: envelope.timestamp, - timestamp: timestamp?.toNumber(), + timestamp: toNumber(timestamp) ?? 0, senderAci: fromAciUuidBytesOrString( senderAciBinary, rawSenderAci, @@ -3404,7 +3448,7 @@ export default class MessageReceiver async #handleViewed( envelope: ProcessedEnvelope, - viewed: ReadonlyArray + viewed: ReadonlyArray ): Promise { const logId = getEnvelopeId(envelope); log.info('handleViewed', logId); @@ -3415,7 +3459,7 @@ export default class MessageReceiver const { timestamp, senderAci: rawSenderAci, senderAciBinary } = data; return { - timestamp: timestamp?.toNumber(), + timestamp: toNumber(timestamp) ?? 0, senderAci: fromAciUuidBytesOrString( senderAciBinary, rawSenderAci, @@ -3437,7 +3481,7 @@ export default class MessageReceiver async #handleCallEvent( envelope: ProcessedEnvelope, - callEvent: Proto.SyncMessage.ICallEvent + callEvent: Proto.SyncMessage.CallEvent ): Promise { const logId = getEnvelopeId(envelope); log.info('handleCallEvent', logId); @@ -3466,7 +3510,7 @@ export default class MessageReceiver async #handleCallLinkUpdate( envelope: ProcessedEnvelope, - callLinkUpdate: Proto.SyncMessage.ICallLinkUpdate + callLinkUpdate: Proto.SyncMessage.CallLinkUpdate ): Promise { const logId = getEnvelopeId(envelope); log.info('handleCallLinkUpdate', logId); @@ -3509,7 +3553,7 @@ export default class MessageReceiver async #handleCallLogEvent( envelope: ProcessedEnvelope, - callLogEvent: Proto.SyncMessage.ICallLogEvent + callLogEvent: Proto.SyncMessage.CallLogEvent ): Promise { const logId = getEnvelopeId(envelope); log.info('handleCallLogEvent', logId); @@ -3534,7 +3578,7 @@ export default class MessageReceiver async #handleDeleteForMeSync( envelope: ProcessedEnvelope, - deleteSync: Proto.SyncMessage.IDeleteForMe + deleteSync: Proto.SyncMessage.DeleteForMe ): Promise { const logId = getEnvelopeId(envelope); log.info('handleDeleteForMeSync', logId); @@ -3733,7 +3777,7 @@ export default class MessageReceiver async #handleAttachmentBackfillResponse( envelope: ProcessedEnvelope, - response: Proto.SyncMessage.IAttachmentBackfillResponse + response: Proto.SyncMessage.AttachmentBackfillResponse ): Promise { const logId = getEnvelopeId(envelope); log.info('handleAttachmentBackfillResponse', logId); @@ -3770,14 +3814,19 @@ export default class MessageReceiver ); let eventData: AttachmentBackfillResponseSyncEventData; - if (response.error != null) { + if (response.data?.error != null) { eventData = { - error: response.error, + error: isKnownProtoEnumMember( + Proto.SyncMessage.AttachmentBackfillResponse.Error, + response.data.error + ) + ? response.data.error + : 0, targetMessage, targetConversation, }; } else { - const { attachments } = response; + const attachments = response.data?.attachments; strictAssert( attachments != null, 'MessageReceiver.handleAttachmentBackfillResponse: no attachments' @@ -3811,7 +3860,7 @@ export default class MessageReceiver async #handleDeviceNameChangeSync( envelope: ProcessedEnvelope, - deviceNameChange: Proto.SyncMessage.IDeviceNameChange + deviceNameChange: Proto.SyncMessage.DeviceNameChange ): Promise { const logId = `handleDeviceNameChangeSync: ${getEnvelopeId(envelope)}`; log.info(logId); @@ -3844,7 +3893,7 @@ export default class MessageReceiver async #handleContacts( envelope: ProcessedEnvelope, - contactSyncProto: Proto.SyncMessage.IContacts + contactSyncProto: Proto.SyncMessage.Contacts ): Promise { const logId = getEnvelopeId(envelope); log.info(`handleContacts ${logId}`); @@ -3870,7 +3919,7 @@ export default class MessageReceiver // proper before/after logic can be applied within that function. async #handleBlocked( envelope: ProcessedEnvelope, - blocked: Proto.SyncMessage.IBlocked + blocked: Proto.SyncMessage.Blocked ): Promise { const logId = `handleBlocked(${getEnvelopeId(envelope)})`; const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type; @@ -4042,7 +4091,7 @@ export default class MessageReceiver #processDecrypted( envelope: ProcessedEnvelope, - decrypted: Proto.IDataMessage + decrypted: Proto.DataMessage ): ProcessedDataMessage { return processDataMessage(decrypted, envelope.timestamp); } @@ -4078,10 +4127,14 @@ function envelopeTypeToCiphertextType(type: number | undefined): number { } function processAddressableMessage( - target: Proto.IAddressableMessage, + target: Proto.AddressableMessage, logId: string ): AddressableMessage | undefined { - const sentAt = target.sentTimestamp?.toNumber(); + if (target.author == null) { + log.error(`${logId}/processAddressableMessage: no author`); + return undefined; + } + const sentAt = toNumber(target.sentTimestamp); if (!isNumber(sentAt)) { log.warn( `${logId}/processAddressableMessage: No sentTimestamp found! Dropping AddressableMessage.` @@ -4089,7 +4142,16 @@ function processAddressableMessage( return undefined; } - const { authorServiceId: rawAuthorServiceId, authorServiceIdBinary } = target; + if (target.author.authorE164 != null) { + return { + type: 'e164' as const, + authorE164: target.author.authorE164, + sentAt, + }; + } + + const { authorServiceId: rawAuthorServiceId, authorServiceIdBinary } = + target.author; const authorServiceId = fromServiceIdBinaryOrString( authorServiceIdBinary, @@ -4117,13 +4179,6 @@ function processAddressableMessage( ); return undefined; } - if (target.authorE164) { - return { - type: 'e164' as const, - authorE164: target.authorE164, - sentAt, - }; - } log.warn( `${logId}/processAddressableMessage: No author field found! Dropping AddressableMessage.` @@ -4132,15 +4187,19 @@ function processAddressableMessage( } function processConversationIdentifier( - target: Proto.IConversationIdentifier, + target: Proto.ConversationIdentifier, logId: string ): ConversationIdentifier | undefined { + if (target.identifier == null) { + log.error(`${logId}/processConversationIdentifier: no identifier`); + return undefined; + } const { threadServiceId: rawThreadServiceId, threadServiceIdBinary, threadGroupId, threadE164, - } = target; + } = target.identifier; const threadServiceId = fromServiceIdBinaryOrString( threadServiceIdBinary, @@ -4186,13 +4245,20 @@ function processConversationIdentifier( } function processBackfilledAttachment( - data: Proto.SyncMessage.AttachmentBackfillResponse.IAttachmentData + data: Proto.SyncMessage.AttachmentBackfillResponse.AttachmentData ): AttachmentBackfillAttachmentType { - if (data.status != null) { - return { status: data.status }; + if (data.data?.status != null) { + return { + status: isKnownProtoEnumMember( + Proto.SyncMessage.AttachmentBackfillResponse.AttachmentData.Status, + data.data.status + ) + ? data.data.status + : 0, + }; } - const attachment = processAttachment(data.attachment); + const attachment = processAttachment(data.data?.attachment); strictAssert( attachment != null, 'MessageReceiver.handleAttachmentBackfillResponse: ' + diff --git a/ts/textsecure/OutgoingMessage.preload.ts b/ts/textsecure/OutgoingMessage.preload.ts index bcfbfea0c0..17619a076d 100644 --- a/ts/textsecure/OutgoingMessage.preload.ts +++ b/ts/textsecure/OutgoingMessage.preload.ts @@ -8,14 +8,12 @@ import lodash from 'lodash'; import { z } from 'zod'; -import type { - CiphertextMessage, - PlaintextContent, -} from '@signalapp/libsignal-client'; +import type { CiphertextMessage } from '@signalapp/libsignal-client'; import { ErrorCode, LibSignalErrorBase, CiphertextMessageType, + PlaintextContent, ProtocolAddress, sealedSenderEncrypt, SenderCertificate, @@ -129,7 +127,7 @@ export default class OutgoingMessage { serviceIds: ReadonlyArray; - message: Proto.Content | PlaintextContent; + message: Proto.Content.Params | PlaintextContent; callback: (result: CallbackResultType) => void; @@ -177,20 +175,14 @@ export default class OutgoingMessage { contentHint: number; groupId: string | undefined; serviceIds: ReadonlyArray; - message: Proto.Content | Proto.DataMessage | PlaintextContent; + message: Proto.Content.Params | PlaintextContent; options?: OutgoingMessageOptionsType; sendLogCallback?: SendLogCallbackType; story?: boolean; timestamp: number; urgent: boolean; }) { - if (message instanceof Proto.DataMessage) { - const content = new Proto.Content(); - content.dataMessage = message; - this.message = content; - } else { - this.message = message; - } + this.message = message; this.timestamp = timestamp; this.serviceIds = serviceIds; @@ -222,17 +214,13 @@ export default class OutgoingMessage { let editMessage: Uint8Array | undefined; let hasPniSignatureMessage = false; - if (proto instanceof Proto.Content) { - if (proto.dataMessage) { - dataMessage = Proto.DataMessage.encode(proto.dataMessage).finish(); - } else if (proto.editMessage) { - editMessage = Proto.EditMessage.encode(proto.editMessage).finish(); + if (!(proto instanceof PlaintextContent)) { + if (proto.content?.dataMessage) { + dataMessage = Proto.DataMessage.encode(proto.content.dataMessage); + } else if (proto.content?.editMessage) { + editMessage = Proto.EditMessage.encode(proto.content.editMessage); } hasPniSignatureMessage = Boolean(proto.pniSignatureMessage); - } else if (proto instanceof Proto.DataMessage) { - dataMessage = Proto.DataMessage.encode(proto).finish(); - } else if (proto instanceof Proto.EditMessage) { - editMessage = Proto.EditMessage.encode(proto).finish(); } this.callback({ @@ -371,21 +359,21 @@ export default class OutgoingMessage { if (!this.plaintext) { const { message } = this; - if (message instanceof Proto.Content) { - this.plaintext = padMessage(Proto.Content.encode(message).finish()); - } else { + if (message instanceof PlaintextContent) { this.plaintext = message.serialize(); + } else { + this.plaintext = padMessage(Proto.Content.encode(message)); } } return this.plaintext; } getContentProtoBytes(): Uint8Array | undefined { - if (this.message instanceof Proto.Content) { - return new Uint8Array(Proto.Content.encode(this.message).finish()); + if (this.message instanceof PlaintextContent) { + return undefined; } - return undefined; + return Proto.Content.encode(this.message); } async getCiphertextMessage({ @@ -399,16 +387,16 @@ export default class OutgoingMessage { }): Promise { const { message } = this; - if (message instanceof Proto.Content) { - return signalEncrypt( - this.getPlaintext(), - protocolAddress, - sessionStore, - identityKeyStore - ); + if (message instanceof PlaintextContent) { + return message.asCiphertextMessage(); } - return message.asCiphertextMessage(); + return signalEncrypt( + this.getPlaintext(), + protocolAddress, + sessionStore, + identityKeyStore + ); } async doSendMessage( diff --git a/ts/textsecure/SendMessage.preload.ts b/ts/textsecure/SendMessage.preload.ts index 597bd4d4e4..b8871c21e8 100644 --- a/ts/textsecure/SendMessage.preload.ts +++ b/ts/textsecure/SendMessage.preload.ts @@ -5,7 +5,6 @@ /* eslint-disable max-classes-per-file */ import { z } from 'zod'; -import Long from 'long'; import PQueue from 'p-queue'; import pMap from 'p-map'; import type { PlaintextContent } from '@signalapp/libsignal-client'; @@ -14,6 +13,7 @@ import { ProtocolAddress, SenderKeyDistributionMessage, } from '@signalapp/libsignal-client'; +import type { RequireExactlyOne } from 'type-fest'; import { GLOBAL_ZONE, @@ -26,6 +26,7 @@ import { parseIntOrThrow } from '../util/parseIntOrThrow.std.js'; import { uuidToBytes } from '../util/uuidToBytes.std.js'; import { Address } from '../types/Address.std.js'; import { QualifiedAddress } from '../types/QualifiedAddress.std.js'; +import type { StoryMessageRecipientsType } from '../types/Stories.std.js'; import { SenderKeys } from '../LibSignalStores.preload.js'; import type { TextAttachmentType, @@ -51,13 +52,8 @@ import type { import OutgoingMessage from './OutgoingMessage.preload.js'; import * as Bytes from '../Bytes.std.js'; import { getRandomBytes } from '../Crypto.node.js'; -import { - MessageError, - SendMessageProtoError, - NoSenderKeyError, -} from './Errors.std.js'; +import { SendMessageProtoError, NoSenderKeyError } from './Errors.std.js'; import { BodyRange } from '../types/BodyRange.std.js'; -import { HTTPError } from '../types/HTTPError.std.js'; import type { RawBodyRange } from '../types/BodyRange.std.js'; import type { StoryContextType } from '../types/Util.std.js'; import { concat, isEmpty } from '../util/iterables.std.js'; @@ -212,7 +208,7 @@ export type SharedMessageOptionsType = Readonly<{ // required timestamp: number; // optional - attachments?: ReadonlyArray; + attachments?: ReadonlyArray; body?: string; bodyRanges?: ReadonlyArray; contact?: ReadonlyArray; @@ -223,11 +219,7 @@ export type SharedMessageOptionsType = Readonly<{ groupV2?: GroupV2InfoType; isViewOnce?: boolean; pinMessage?: SendPinMessageType; - pollVote?: OutgoingPollVote; pollCreate?: PollCreateType; - pollTerminate?: Readonly<{ - targetTimestamp: number; - }>; preview?: ReadonlyArray; profileKey?: Uint8Array; quote?: OutgoingQuoteType; @@ -252,24 +244,28 @@ export type GroupMessageOptionsType = Readonly< } >; -export type PollVoteBuildOptions = Required< - Pick -> & +export type PollVoteBuildOptions = Readonly<{ + timestamp: number; + pollVote: OutgoingPollVote; +}> & Pick< MessageOptionsType, 'groupV2' | 'profileKey' | 'expireTimer' | 'expireTimerVersion' >; -export type PollTerminateBuildOptions = Required< - Pick -> & +export type PollTerminateBuildOptions = Readonly<{ + timestamp: number; + pollTerminate: Readonly<{ + targetTimestamp: number; + }>; +}> & Pick< MessageOptionsType, 'groupV2' | 'profileKey' | 'expireTimer' | 'expireTimerVersion' >; class Message { - attachments: ReadonlyArray; + attachments: ReadonlyArray; body?: string; @@ -310,7 +306,7 @@ class Message { timestamp: number; - dataMessage?: Proto.DataMessage; + dataMessage?: Proto.DataMessage.Params; deleteForEveryone?: SendDeleteForEveryoneType; @@ -337,13 +333,10 @@ class Message { this.sticker = options.sticker; this.reaction = options.reaction; this.pollCreate = options.pollCreate; - this.pollTerminate = options.pollTerminate; this.timestamp = options.timestamp; this.deleteForEveryone = options.deleteForEveryone; this.groupCallUpdate = options.groupCallUpdate; this.storyContext = options.storyContext; - // Polls - this.pollVote = options.pollVote; this.pinMessage = options.pinMessage; this.unpinMessage = options.unpinMessage; @@ -391,18 +384,14 @@ class Message { return (this.flags || 0) & Proto.DataMessage.Flags.END_SESSION; } - toProto(): Proto.DataMessage { + toProto(): Proto.DataMessage.Params { if (this.dataMessage) { return this.dataMessage; } - const proto = new Proto.DataMessage(); - proto.timestamp = Long.fromNumber(this.timestamp); - proto.attachments = this.attachments.slice(); + let requiredProtocolVersion = 0; if (this.body) { - proto.body = this.body; - const mentionCount = this.bodyRanges ? this.bodyRanges.filter(BodyRange.isMention).length : 0; @@ -420,321 +409,311 @@ class Message { `and ${otherRangeCount} other ranges${storyInfo}` ); } - if (this.flags) { - proto.flags = this.flags; - } - if (this.groupV2) { - proto.groupV2 = new Proto.GroupContextV2(); - proto.groupV2.masterKey = this.groupV2.masterKey; - proto.groupV2.revision = this.groupV2.revision; - const { groupChange } = this.groupV2; - if (groupChange) { - if (groupChange.byteLength <= MAX_EMBEDDED_GROUP_CHANGE_BYTES) { - proto.groupV2.groupChange = groupChange; - } else { - // As a message-size optimization, we do not embed large updates and receiving - // devices fetch them from the group server instead - log.info( - `Discarding oversized group change proto (${groupChange.byteLength} bytes)` - ); - } - } - } - if (this.sticker) { - proto.sticker = new Proto.DataMessage.Sticker(); - proto.sticker.packId = Bytes.fromHex(this.sticker.packId); - proto.sticker.packKey = Bytes.fromBase64(this.sticker.packKey); - proto.sticker.stickerId = this.sticker.stickerId; - proto.sticker.emoji = this.sticker.emoji; - proto.sticker.data = this.sticker.data; - } - if (this.reaction) { - proto.reaction = new Proto.DataMessage.Reaction(); - proto.reaction.emoji = this.reaction.emoji || null; - proto.reaction.remove = this.reaction.remove || false; - if (isProtoBinaryEncodingEnabled()) { - proto.reaction.targetAuthorAciBinary = this.reaction.targetAuthorAci - ? toAciObject(this.reaction.targetAuthorAci).getRawUuidBytes() - : null; - } else { - proto.reaction.targetAuthorAci = this.reaction.targetAuthorAci || null; - } - proto.reaction.targetSentTimestamp = - this.reaction.targetTimestamp === undefined - ? null - : Long.fromNumber(this.reaction.targetTimestamp); + if ( + this.groupV2?.groupChange && + this.groupV2.groupChange.byteLength > MAX_EMBEDDED_GROUP_CHANGE_BYTES + ) { + // As a message-size optimization, we do not embed large updates and receiving + // devices fetch them from the group server instead + log.info( + 'Discarding oversized group change proto ' + + `(${this.groupV2.groupChange.byteLength} bytes)` + ); } - if (Array.isArray(this.preview)) { - proto.preview = this.preview.map(preview => { - const item = new Proto.Preview(); - item.title = preview.title; - item.url = preview.url; - item.description = preview.description || null; - item.date = preview.date || null; - if (preview.image) { - item.image = preview.image; - } - return item; - }); - } + let contact: Array | null = null; if (Array.isArray(this.contact)) { - proto.contact = this.contact.map( - (contact: EmbeddedContactWithUploadedAvatar) => { - const contactProto = new Proto.DataMessage.Contact(); - if (contact.name) { - const nameProto: Proto.DataMessage.Contact.IName = { - givenName: contact.name.givenName, - familyName: contact.name.familyName, - prefix: contact.name.prefix, - suffix: contact.name.suffix, - middleName: contact.name.middleName, - nickname: contact.name.nickname, + contact = this.contact.map( + (contactEntry: EmbeddedContactWithUploadedAvatar) => { + let name: Proto.DataMessage.Contact.Name.Params | null = null; + if (contactEntry.name) { + name = { + givenName: contactEntry.name.givenName ?? null, + familyName: contactEntry.name.familyName ?? null, + prefix: contactEntry.name.prefix ?? null, + suffix: contactEntry.name.suffix ?? null, + middleName: contactEntry.name.middleName ?? null, + nickname: contactEntry.name.nickname ?? null, }; - contactProto.name = new Proto.DataMessage.Contact.Name(nameProto); } - if (Array.isArray(contact.number)) { - contactProto.number = contact.number.map(number => { - const numberProto: Proto.DataMessage.Contact.IPhone = { - value: number.value, - type: numberToPhoneType(number.type), - label: number.label, - }; - - return new Proto.DataMessage.Contact.Phone(numberProto); - }); + let number: Array | null = + null; + if (Array.isArray(contactEntry.number)) { + number = contactEntry.number.map( + (entry): Proto.DataMessage.Contact.Phone.Params => { + return { + value: entry.value, + type: numberToPhoneType(entry.type), + label: entry.label, + }; + } + ); } - if (Array.isArray(contact.email)) { - contactProto.email = contact.email.map(email => { - const emailProto: Proto.DataMessage.Contact.IEmail = { - value: email.value, - type: numberToEmailType(email.type), - label: email.label, - }; - - return new Proto.DataMessage.Contact.Email(emailProto); - }); + let email: Array | null = + null; + if (Array.isArray(contactEntry.email)) { + email = contactEntry.email.map( + (entry): Proto.DataMessage.Contact.Email.Params => { + return { + value: entry.value, + type: numberToEmailType(entry.type), + label: entry.label, + }; + } + ); } - if (Array.isArray(contact.address)) { - contactProto.address = contact.address.map(address => { - const addressProto: Proto.DataMessage.Contact.IPostalAddress = { - type: numberToAddressType(address.type), - label: address.label, - street: address.street, - pobox: address.pobox, - neighborhood: address.neighborhood, - city: address.city, - region: address.region, - postcode: address.postcode, - country: address.country, - }; - - return new Proto.DataMessage.Contact.PostalAddress(addressProto); - }); + let address: Array | null = + null; + if (Array.isArray(contactEntry.address)) { + address = contactEntry.address.map( + (entry): Proto.DataMessage.Contact.PostalAddress.Params => { + return { + type: numberToAddressType(entry.type), + label: entry.label, + street: entry.street, + pobox: entry.pobox, + neighborhood: entry.neighborhood, + city: entry.city, + region: entry.region, + postcode: entry.postcode, + country: entry.country, + }; + } + ); } - if (contact.avatar?.avatar) { - const avatarProto = new Proto.DataMessage.Contact.Avatar(); - avatarProto.avatar = contact.avatar.avatar; - avatarProto.isProfile = Boolean(contact.avatar.isProfile); - contactProto.avatar = avatarProto; + let avatar: Proto.DataMessage.Contact.Avatar.Params | null = null; + if (contactEntry.avatar?.avatar) { + avatar = { + avatar: contactEntry.avatar.avatar, + isProfile: Boolean(contactEntry.avatar.isProfile), + }; } - if (contact.organization) { - contactProto.organization = contact.organization; + let organization: string | null = null; + if (contactEntry.organization) { + organization = contactEntry.organization; } - return contactProto; + return { + name, + email, + number, + address, + avatar, + organization, + }; } ); } + let quote: Proto.DataMessage.Quote.Params | null = null; if (this.quote) { - const ProtoBodyRange = Proto.BodyRange; - const { Quote } = Proto.DataMessage; + quote = { + type: this.quote.isGiftBadge + ? Proto.DataMessage.Quote.Type.GIFT_BADGE + : Proto.DataMessage.Quote.Type.NORMAL, + id: this.quote.id === undefined ? null : BigInt(this.quote.id), + authorAciBinary: + this.quote.authorAci && isProtoBinaryEncodingEnabled() + ? toAciObject(this.quote.authorAci).getRawUuidBytes() + : null, + authorAci: isProtoBinaryEncodingEnabled() + ? null + : (this.quote.authorAci ?? null), + text: this.quote.text ?? null, + attachments: this.quote.attachments.map(attachment => { + return { + contentType: attachment.contentType, + fileName: attachment.fileName ?? null, + thumbnail: attachment.thumbnail ?? null, + }; + }), + bodyRanges: this.quote.bodyRanges?.map(toBodyRange) ?? null, + }; - proto.quote = new Quote(); - const { quote } = proto; - - if (this.quote.isGiftBadge) { - quote.type = Proto.DataMessage.Quote.Type.GIFT_BADGE; - } else { - quote.type = Proto.DataMessage.Quote.Type.NORMAL; - } - - quote.id = - this.quote.id === undefined ? null : Long.fromNumber(this.quote.id); - if (isProtoBinaryEncodingEnabled()) { - quote.authorAciBinary = this.quote.authorAci - ? toAciObject(this.quote.authorAci).getRawUuidBytes() - : null; - } else { - quote.authorAci = this.quote.authorAci || null; - } - quote.text = this.quote.text || null; - quote.attachments = this.quote.attachments.slice() || []; - const bodyRanges = this.quote.bodyRanges || []; - quote.bodyRanges = bodyRanges.map(range => { - const bodyRange = new ProtoBodyRange(); - bodyRange.start = range.start; - bodyRange.length = range.length; - if (BodyRange.isMention(range)) { - if (isProtoBinaryEncodingEnabled()) { - bodyRange.mentionAciBinary = toAciObject( - range.mentionAci - ).getRawUuidBytes(); - } else { - bodyRange.mentionAci = range.mentionAci; - } - } else if (BodyRange.isFormatting(range)) { - bodyRange.style = range.style; - } else { - throw missingCaseError(range); - } - return bodyRange; - }); - if ( - quote.bodyRanges.length && - (!proto.requiredProtocolVersion || - proto.requiredProtocolVersion < - Proto.DataMessage.ProtocolVersion.MENTIONS) - ) { - proto.requiredProtocolVersion = - Proto.DataMessage.ProtocolVersion.MENTIONS; + if (quote?.bodyRanges?.length) { + requiredProtocolVersion = Math.max( + requiredProtocolVersion, + Proto.DataMessage.ProtocolVersion.MENTIONS + ); } } - if (this.expireTimer) { - proto.expireTimer = this.expireTimer; - } - if (this.expireTimerVersion) { - proto.expireTimerVersion = this.expireTimerVersion; - } - if (this.profileKey) { - proto.profileKey = this.profileKey; - } - if (this.isViewOnce) { - proto.isViewOnce = true; - } + + let adminDelete: Proto.DataMessage.AdminDelete.Params | null = null; + let del: Proto.DataMessage.Delete.Params | null = null; if (this.deleteForEveryone) { const { isAdminDelete, targetSentTimestamp, targetAuthorAci } = this.deleteForEveryone; if (isAdminDelete) { - proto.adminDelete = { - targetSentTimestamp: Long.fromNumber(targetSentTimestamp), + adminDelete = { + targetSentTimestamp: BigInt(targetSentTimestamp), targetAuthorAciBinary: uuidToBytes(targetAuthorAci), }; } else { - proto.delete = { - targetSentTimestamp: Long.fromNumber(targetSentTimestamp), + del = { + targetSentTimestamp: BigInt(targetSentTimestamp), }; } } - if (this.bodyRanges) { - proto.requiredProtocolVersion = - Proto.DataMessage.ProtocolVersion.MENTIONS; - proto.bodyRanges = this.bodyRanges.map(bodyRange => { - const { start, length } = bodyRange; - if (BodyRange.isMention(bodyRange)) { - if (isProtoBinaryEncodingEnabled()) { - return { - start, - length, - mentionAciBinary: toAciObject( - bodyRange.mentionAci - ).getRawUuidBytes(), - }; - } - return { - start, - length, - mentionAci: bodyRange.mentionAci, - }; - } - if (BodyRange.isFormatting(bodyRange)) { - return { - start, - length, - style: bodyRange.style, - }; - } - throw missingCaseError(bodyRange); - }); + if (this.bodyRanges?.length) { + requiredProtocolVersion = Math.max( + requiredProtocolVersion, + Proto.DataMessage.ProtocolVersion.MENTIONS + ); } + let groupCallUpdate: Proto.DataMessage.GroupCallUpdate.Params | null = null; if (this.groupCallUpdate) { - const { GroupCallUpdate } = Proto.DataMessage; - - const groupCallUpdate = new GroupCallUpdate(); - groupCallUpdate.eraId = this.groupCallUpdate.eraId; - - proto.groupCallUpdate = groupCallUpdate; + groupCallUpdate = { + eraId: this.groupCallUpdate.eraId, + }; } + let storyContext: Proto.DataMessage.StoryContext.Params | null = null; if (this.storyContext) { - const { StoryContext } = Proto.DataMessage; - - const storyContext = new StoryContext(); - if (this.storyContext.authorAci) { - if (isProtoBinaryEncodingEnabled()) { - storyContext.authorAciBinary = toAciObject( - this.storyContext.authorAci - ).getRawUuidBytes(); - } else { - storyContext.authorAci = this.storyContext.authorAci; - } - } - storyContext.sentTimestamp = Long.fromNumber(this.storyContext.timestamp); - - proto.storyContext = storyContext; + storyContext = { + sentTimestamp: BigInt(this.storyContext.timestamp), + authorAciBinary: + this.storyContext.authorAci && isProtoBinaryEncodingEnabled() + ? toAciObject(this.storyContext.authorAci).getRawUuidBytes() + : null, + authorAci: isProtoBinaryEncodingEnabled() + ? null + : (this.storyContext.authorAci ?? null), + }; } + let pollCreate: Proto.DataMessage.PollCreate.Params | null = null; if (this.pollCreate) { - const create = new Proto.DataMessage.PollCreate(); - create.question = this.pollCreate.question; - create.allowMultiple = Boolean(this.pollCreate.allowMultiple); - create.options = this.pollCreate.options.slice(); - proto.pollCreate = create; - proto.requiredProtocolVersion = Proto.DataMessage.ProtocolVersion.POLLS; + pollCreate = { + question: this.pollCreate.question, + allowMultiple: Boolean(this.pollCreate.allowMultiple), + options: this.pollCreate.options.slice(), + }; + requiredProtocolVersion = Math.max( + requiredProtocolVersion, + Proto.DataMessage.ProtocolVersion.POLLS + ); } + let pinMessage: Proto.DataMessage.PinMessage.Params | null = null; if (this.pinMessage != null) { const { targetAuthorAci, targetSentTimestamp, pinDurationSeconds } = this.pinMessage; - const pinMessage = new Proto.DataMessage.PinMessage({ + pinMessage = { targetAuthorAciBinary: toAciObject(targetAuthorAci).getRawUuidBytes(), - targetSentTimestamp: Long.fromNumber(targetSentTimestamp), - }); - - if (pinDurationSeconds != null) { - pinMessage.pinDurationSeconds = pinDurationSeconds; - } else { - pinMessage.pinDurationForever = true; - } - - proto.pinMessage = pinMessage; + targetSentTimestamp: BigInt(targetSentTimestamp), + pinDuration: + pinDurationSeconds != null + ? { + pinDurationSeconds, + } + : { + pinDurationForever: true, + }, + }; } + let unpinMessage: Proto.DataMessage.UnpinMessage.Params | null = null; if (this.unpinMessage != null) { const { targetAuthorAci, targetSentTimestamp } = this.unpinMessage; - const unpinMessage = new Proto.DataMessage.UnpinMessage({ + unpinMessage = { targetAuthorAciBinary: toAciObject(targetAuthorAci).getRawUuidBytes(), - targetSentTimestamp: Long.fromNumber(targetSentTimestamp), - }); - - proto.unpinMessage = unpinMessage; + targetSentTimestamp: BigInt(targetSentTimestamp), + }; } - this.dataMessage = proto; - return proto; + const dataMessage: Proto.DataMessage.Params = { + timestamp: BigInt(this.timestamp), + attachments: this.attachments.slice(), + flags: this.flags ?? 0, + body: this.body ?? null, + bodyRanges: this.bodyRanges?.map(toBodyRange) ?? null, + groupV2: this.groupV2 + ? { + masterKey: this.groupV2.masterKey, + revision: this.groupV2.revision, + groupChange: + this.groupV2.groupChange != null && + this.groupV2.groupChange.byteLength < + MAX_EMBEDDED_GROUP_CHANGE_BYTES + ? this.groupV2.groupChange + : null, + } + : null, + sticker: this.sticker + ? ({ + packId: Bytes.fromHex(this.sticker.packId), + packKey: Bytes.fromBase64(this.sticker.packKey), + stickerId: this.sticker.stickerId, + emoji: this.sticker.emoji ?? null, + data: this.sticker.data, + } satisfies Proto.DataMessage.Sticker.Params) + : null, + reaction: this.reaction + ? ({ + emoji: this.reaction.emoji ?? null, + remove: Boolean(this.reaction.remove), + targetAuthorAciBinary: + this.reaction.targetAuthorAci && isProtoBinaryEncodingEnabled() + ? toAciObject(this.reaction.targetAuthorAci).getRawUuidBytes() + : null, + targetAuthorAci: isProtoBinaryEncodingEnabled() + ? null + : (this.reaction.targetAuthorAci ?? null), + targetSentTimestamp: + this.reaction.targetTimestamp == null + ? null + : BigInt(this.reaction.targetTimestamp), + } satisfies Proto.DataMessage.Reaction.Params) + : null, + + preview: + this.preview?.map((preview): Proto.Preview.Params => { + return { + title: preview.title ?? null, + url: preview.url, + description: preview.description ?? null, + date: preview.date ? BigInt(preview.date) : null, + image: preview.image ?? null, + }; + }) ?? null, + + contact, + quote, + adminDelete, + delete: del, + groupCallUpdate, + storyContext, + pollCreate, + pinMessage, + unpinMessage, + + // Handled separately + pollVote: null, + pollTerminate: null, + + expireTimer: this.expireTimer ?? null, + expireTimerVersion: this.expireTimerVersion ?? null, + profileKey: this.profileKey ?? null, + isViewOnce: Boolean(this.isViewOnce), + requiredProtocolVersion, + + payment: null, + giftBadge: null, + }; + this.dataMessage = dataMessage; + return dataMessage; } } export type AddPniSignatureMessageToProtoOptionsType = Readonly<{ conversation?: ConversationModel; - proto: Proto.Content; + proto: Proto.Content.Params; reason: string; }>; @@ -809,48 +788,49 @@ export class MessageSender { getTextAttachmentProto( attachmentAttrs: OutgoingTextAttachmentType - ): Proto.TextAttachment { - const textAttachment = new Proto.TextAttachment(); + ): Proto.TextAttachment.Params { + const { preview, gradient } = attachmentAttrs; - if (attachmentAttrs.text) { - textAttachment.text = attachmentAttrs.text; - } - - textAttachment.textStyle = attachmentAttrs.textStyle - ? Number(attachmentAttrs.textStyle) - : 0; - - if (attachmentAttrs.textForegroundColor) { - textAttachment.textForegroundColor = attachmentAttrs.textForegroundColor; - } - - if (attachmentAttrs.textBackgroundColor) { - textAttachment.textBackgroundColor = attachmentAttrs.textBackgroundColor; - } - - if (attachmentAttrs.preview) { - textAttachment.preview = { - image: attachmentAttrs.preview.image, - title: attachmentAttrs.preview.title, - url: attachmentAttrs.preview.url, + let background: Proto.TextAttachment.Params['background']; + if (gradient) { + background = { + gradient: { + startColor: gradient.startColor ?? null, + endColor: gradient.endColor ?? null, + angle: gradient.angle ?? null, + colors: gradient.colors?.slice() ?? null, + positions: gradient.positions?.slice() ?? null, + }, }; - } - - if (attachmentAttrs.gradient) { - const { colors, positions, ...rest } = attachmentAttrs.gradient; - - textAttachment.gradient = { - ...rest, - colors: colors?.slice(), - positions: positions?.slice(), + } else if (attachmentAttrs.color) { + background = { + color: attachmentAttrs.color, }; - textAttachment.background = 'gradient'; } else { - textAttachment.color = attachmentAttrs.color; - textAttachment.background = 'color'; + background = null; } - return textAttachment; + return { + text: attachmentAttrs.text ?? null, + textStyle: attachmentAttrs.textStyle + ? Number(attachmentAttrs.textStyle) + : 0, + + textForegroundColor: attachmentAttrs.textForegroundColor ?? null, + textBackgroundColor: attachmentAttrs.textBackgroundColor ?? null, + + preview: preview + ? { + image: preview.image ?? null, + title: preview.title ?? null, + url: preview.url, + description: null, + date: null, + } + : null, + + background, + }; } async getDataOrEditMessage( @@ -860,14 +840,12 @@ export class MessageSender { const dataMessage = message.toProto(); if (options.targetTimestampForEdit) { - const editMessage = new Proto.EditMessage(); - editMessage.dataMessage = dataMessage; - editMessage.targetSentTimestamp = Long.fromNumber( - options.targetTimestampForEdit - ); - return Proto.EditMessage.encode(editMessage).finish(); + return Proto.EditMessage.encode({ + dataMessage, + targetSentTimestamp: BigInt(options.targetTimestampForEdit), + }); } - return Proto.DataMessage.encode(dataMessage).finish(); + return Proto.DataMessage.encode(dataMessage); } createDataMessageProtoForPollVote({ @@ -877,37 +855,50 @@ export class MessageSender { expireTimer, expireTimerVersion, pollVote, - }: PollVoteBuildOptions): Proto.DataMessage { - const dataMessage = new Proto.DataMessage(); - dataMessage.timestamp = Long.fromNumber(timestamp); + }: PollVoteBuildOptions): Proto.DataMessage.Params { + return { + timestamp: BigInt(timestamp), + groupV2: groupV2 + ? { + masterKey: groupV2.masterKey, + revision: groupV2.revision, + groupChange: null, + } + : null, + expireTimer: expireTimer ?? null, + expireTimerVersion: expireTimerVersion ?? null, + profileKey: profileKey ?? null, + pollVote: { + targetAuthorAciBinary: toAciObject( + pollVote.targetAuthorAci + ).getRawUuidBytes(), + targetSentTimestamp: BigInt(pollVote.targetTimestamp), + optionIndexes: pollVote.optionIndexes.slice(), + voteCount: pollVote.voteCount, + }, - if (groupV2) { - const groupContext = new Proto.GroupContextV2(); - groupContext.masterKey = groupV2.masterKey; - groupContext.revision = groupV2.revision; - dataMessage.groupV2 = groupContext; - } - - if (typeof expireTimer !== 'undefined') { - dataMessage.expireTimer = expireTimer; - } - if (typeof expireTimerVersion !== 'undefined') { - dataMessage.expireTimerVersion = expireTimerVersion; - } - if (profileKey) { - dataMessage.profileKey = profileKey; - } - - const vote = new Proto.DataMessage.PollVote(); - vote.targetAuthorAciBinary = toAciObject( - pollVote.targetAuthorAci - ).getRawUuidBytes(); - vote.targetSentTimestamp = Long.fromNumber(pollVote.targetTimestamp); - vote.optionIndexes = pollVote.optionIndexes.slice(); - vote.voteCount = pollVote.voteCount; - dataMessage.pollVote = vote; - - return dataMessage; + body: null, + attachments: null, + flags: null, + quote: null, + contact: null, + preview: null, + sticker: null, + requiredProtocolVersion: null, + isViewOnce: null, + reaction: null, + delete: null, + bodyRanges: null, + groupCallUpdate: null, + payment: null, + storyContext: null, + giftBadge: null, + pollCreate: null, + pollTerminate: null, + pinMessage: null, + unpinMessage: null, + adminDelete: null, + }; } async getPollVoteDataMessage({ @@ -926,7 +917,7 @@ export class MessageSender { expireTimerVersion, pollVote, }); - return Proto.DataMessage.encode(proto).finish(); + return Proto.DataMessage.encode(proto); } async getPollVoteContentMessage({ @@ -936,7 +927,7 @@ export class MessageSender { expireTimer, expireTimerVersion, pollVote, - }: PollVoteBuildOptions): Promise { + }: PollVoteBuildOptions): Promise { const dataMessage = this.createDataMessageProtoForPollVote({ groupV2, timestamp, @@ -945,9 +936,13 @@ export class MessageSender { expireTimerVersion, pollVote, }); - const contentMessage = new Proto.Content(); - contentMessage.dataMessage = dataMessage; - return contentMessage; + return { + content: { + dataMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }; } createDataMessageProtoForPollTerminate({ @@ -957,34 +952,45 @@ export class MessageSender { expireTimer, expireTimerVersion, pollTerminate, - }: PollTerminateBuildOptions): Proto.DataMessage { - const dataMessage = new Proto.DataMessage(); - dataMessage.timestamp = Long.fromNumber(timestamp); + }: PollTerminateBuildOptions): Proto.DataMessage.Params { + return { + timestamp: BigInt(timestamp), + groupV2: groupV2 + ? { + masterKey: groupV2.masterKey, + revision: groupV2.revision, + groupChange: null, + } + : null, + expireTimer: expireTimer ?? null, + expireTimerVersion: expireTimerVersion ?? null, + profileKey: profileKey ?? null, + pollTerminate: { + targetSentTimestamp: BigInt(pollTerminate.targetTimestamp), + }, - if (groupV2) { - const groupContext = new Proto.GroupContextV2(); - groupContext.masterKey = groupV2.masterKey; - groupContext.revision = groupV2.revision; - dataMessage.groupV2 = groupContext; - } - - if (typeof expireTimer !== 'undefined') { - dataMessage.expireTimer = expireTimer; - } - if (typeof expireTimerVersion !== 'undefined') { - dataMessage.expireTimerVersion = expireTimerVersion; - } - if (profileKey) { - dataMessage.profileKey = profileKey; - } - - const terminate = new Proto.DataMessage.PollTerminate(); - terminate.targetSentTimestamp = Long.fromNumber( - pollTerminate.targetTimestamp - ); - dataMessage.pollTerminate = terminate; - - return dataMessage; + body: null, + attachments: null, + flags: null, + quote: null, + contact: null, + preview: null, + sticker: null, + requiredProtocolVersion: null, + isViewOnce: null, + reaction: null, + delete: null, + bodyRanges: null, + groupCallUpdate: null, + payment: null, + storyContext: null, + giftBadge: null, + pollCreate: null, + pollVote: null, + pinMessage: null, + unpinMessage: null, + adminDelete: null, + }; } async getPollTerminateContentMessage({ @@ -994,7 +1000,7 @@ export class MessageSender { expireTimer, expireTimerVersion, pollTerminate, - }: PollTerminateBuildOptions): Promise { + }: PollTerminateBuildOptions): Promise { const dataMessage = this.createDataMessageProtoForPollTerminate({ groupV2, timestamp, @@ -1003,9 +1009,13 @@ export class MessageSender { expireTimerVersion, pollTerminate, }); - const contentMessage = new Proto.Content(); - contentMessage.dataMessage = dataMessage; - return contentMessage; + return { + content: { + dataMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }; } async getStoryMessage({ @@ -1022,45 +1032,34 @@ export class MessageSender { groupV2?: GroupV2InfoType; profileKey: Uint8Array; textAttachment?: OutgoingTextAttachmentType; - }): Promise { - const storyMessage = new Proto.StoryMessage(); - - storyMessage.profileKey = profileKey; - - if (fileAttachment) { - if (bodyRanges) { - storyMessage.bodyRanges = bodyRanges; - } - try { - storyMessage.fileAttachment = fileAttachment; - } catch (error) { - if (error instanceof HTTPError) { - throw new MessageError(storyMessage, error); - } else { - throw error; - } - } - } + }): Promise { + let attachment: Proto.StoryMessage.Params['attachment']; if (textAttachment) { - storyMessage.textAttachment = this.getTextAttachmentProto(textAttachment); + attachment = { + textAttachment: this.getTextAttachmentProto(textAttachment), + }; + } else if (fileAttachment) { + attachment = { fileAttachment }; + } else { + attachment = null; } - if (groupV2) { - const groupV2Context = new Proto.GroupContextV2(); - groupV2Context.masterKey = groupV2.masterKey; - groupV2Context.revision = groupV2.revision; - - if (groupV2.groupChange) { - groupV2Context.groupChange = groupV2.groupChange; - } - - storyMessage.group = groupV2Context; - } - - storyMessage.allowsReplies = Boolean(allowsReplies); - - return storyMessage; + return { + profileKey, + allowsReplies: Boolean(allowsReplies), + group: groupV2 + ? { + masterKey: groupV2.masterKey, + revision: groupV2.revision, + groupChange: groupV2.groupChange ?? null, + } + : null, + bodyRanges: fileAttachment + ? (bodyRanges?.map(toBodyRange) ?? null) + : null, + attachment, + }; } async getContentMessage( @@ -1068,21 +1067,28 @@ export class MessageSender { Readonly<{ includePniSignatureMessage?: boolean; }> - ): Promise { + ): Promise { const message = await this.getHydratedMessage(options); const dataMessage = message.toProto(); - const contentMessage = new Proto.Content(); - if (options.targetTimestampForEdit) { - const editMessage = new Proto.EditMessage(); - editMessage.dataMessage = dataMessage; - editMessage.targetSentTimestamp = Long.fromNumber( - options.targetTimestampForEdit - ); - contentMessage.editMessage = editMessage; - } else { - contentMessage.dataMessage = dataMessage; - } + const contentMessage: Proto.Content.Params = options.targetTimestampForEdit + ? { + content: { + editMessage: { + dataMessage, + targetSentTimestamp: BigInt(options.targetTimestampForEdit), + }, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + } + : { + content: { + dataMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }; const { includePniSignatureMessage } = options; if (includePniSignatureMessage) { @@ -1121,7 +1127,7 @@ export class MessageSender { isTyping: boolean; timestamp?: number; }> - ): Proto.Content { + ): Proto.Content.Params { const ACTION_ENUM = Proto.TypingMessage.Action; const { recipientId, groupId, isTyping, timestamp } = options; @@ -1134,25 +1140,27 @@ export class MessageSender { const finalTimestamp = timestamp || Date.now(); const action = isTyping ? ACTION_ENUM.STARTED : ACTION_ENUM.STOPPED; - const typingMessage = new Proto.TypingMessage(); - if (groupId) { - typingMessage.groupId = groupId; - } - typingMessage.action = action; - typingMessage.timestamp = Long.fromNumber(finalTimestamp); - - const contentMessage = new Proto.Content(); - contentMessage.typingMessage = typingMessage; + const content: Proto.Content.Params = { + content: { + typingMessage: { + groupId: groupId ?? null, + action, + timestamp: BigInt(finalTimestamp), + }, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }; if (recipientId) { addPniSignatureMessageToProto({ conversation: window.ConversationController.get(recipientId), - proto: contentMessage, + proto: content, reason: `getTypingContentMessage(${finalTimestamp})`, }); } - return contentMessage; + return content; } getAttrsFromGroupOptions( @@ -1179,9 +1187,7 @@ export class MessageSender { timestamp, pinMessage, unpinMessage, - pollVote, pollCreate, - pollTerminate, } = options; if (!groupV2) { @@ -1228,18 +1234,21 @@ export class MessageSender { timestamp, pinMessage, unpinMessage, - pollVote, pollCreate, - pollTerminate, }; } - static createSyncMessage(): Proto.SyncMessage { - const syncMessage = new Proto.SyncMessage(); - - syncMessage.padding = this.getRandomPadding(); - - return syncMessage; + static padSyncMessage( + params: RequireExactlyOne> + ): Proto.SyncMessage.Params { + return { + read: null, + viewed: null, + stickerPackOperation: null, + content: null, + ...params, + padding: this.getRandomPadding(), + }; } // Low-level sends @@ -1307,7 +1316,7 @@ export class MessageSender { contentHint: number; groupId: string | undefined; options?: SendOptionsType; - proto: Proto.Content | Proto.DataMessage | PlaintextContent; + proto: Proto.Content.Params | PlaintextContent; recipients: ReadonlyArray; sendLogCallback?: SendLogCallbackType; story?: boolean; @@ -1368,7 +1377,7 @@ export class MessageSender { }: Readonly<{ timestamp: number; recipients: Array; - proto: Proto.Content | Proto.DataMessage | PlaintextContent; + proto: Proto.Content.Params | PlaintextContent; contentHint: number; groupId: string | undefined; options?: SendOptionsType; @@ -1413,7 +1422,7 @@ export class MessageSender { groupId?: string; serviceId: ServiceIdString | undefined; options?: SendOptionsType; - proto: Proto.DataMessage | Proto.Content | PlaintextContent; + proto: Proto.Content.Params | PlaintextContent; timestamp: number; urgent: boolean; }>): Promise { @@ -1506,91 +1515,130 @@ export class MessageSender { isUpdate?: boolean; urgent: boolean; options?: SendOptionsType; - storyMessage?: Proto.StoryMessage; - storyMessageRecipients?: ReadonlyArray; + storyMessage?: Proto.StoryMessage.Params; + storyMessageRecipients?: StoryMessageRecipientsType; }>): Promise { const myAci = itemStorage.user.getCheckedAci(); - const sentMessage = new Proto.SyncMessage.Sent(); - sentMessage.timestamp = Long.fromNumber(timestamp); - + let editMessage: Proto.EditMessage.Params | null; + let message: Proto.DataMessage.Params | null; if (encodedEditMessage) { - const editMessage = Proto.EditMessage.decode(encodedEditMessage); - sentMessage.editMessage = editMessage; + editMessage = Proto.EditMessage.decode(encodedEditMessage); + message = null; } else if (encodedDataMessage) { - const dataMessage = Proto.DataMessage.decode(encodedDataMessage); - sentMessage.message = dataMessage; - } - if (destinationE164) { - sentMessage.destinationE164 = destinationE164; - } - if (destinationServiceId) { - if (isProtoBinaryEncodingEnabled()) { - sentMessage.destinationServiceIdBinary = - toServiceIdObject(destinationServiceId).getServiceIdBinary(); - } else { - sentMessage.destinationServiceId = destinationServiceId; - } - } - if (expirationStartTimestamp) { - sentMessage.expirationStartTimestamp = Long.fromNumber( - expirationStartTimestamp - ); - } - if (storyMessage) { - sentMessage.storyMessage = storyMessage; - } - if (storyMessageRecipients) { - sentMessage.storyMessageRecipients = storyMessageRecipients.slice(); - } - - if (isUpdate) { - sentMessage.isRecipientUpdate = true; + message = Proto.DataMessage.decode(encodedDataMessage); + editMessage = null; + } else { + message = null; + editMessage = null; } // Though this field has 'unidentified' in the name, it should have entries for each // number we sent to. - if (!isEmpty(conversationIdsSentTo)) { - sentMessage.unidentifiedStatus = await pMap( + let unidentifiedStatus: Array | null; + if (isEmpty(conversationIdsSentTo)) { + unidentifiedStatus = null; + } else { + unidentifiedStatus = await pMap( conversationIdsSentTo, - async conversationId => { - const status = - new Proto.SyncMessage.Sent.UnidentifiedDeliveryStatus(); + async ( + conversationId + ): Promise => { const conv = window.ConversationController.get(conversationId); + const serviceId = conv?.getServiceId(); + let destinationPniIdentityKey: Uint8Array | null = null; if (conv) { - const serviceId = conv.getServiceId(); - if (serviceId) { - if (isProtoBinaryEncodingEnabled()) { - status.destinationServiceIdBinary = - toServiceIdObject(serviceId).getServiceIdBinary(); - } else { - status.destinationServiceId = serviceId; - } - } if (isPniString(serviceId)) { const pniIdentityKey = await signalProtocolStore.loadIdentityKey(serviceId); if (pniIdentityKey) { - status.destinationPniIdentityKey = pniIdentityKey; + destinationPniIdentityKey = pniIdentityKey; } } } - status.unidentified = - conversationIdsWithSealedSender.has(conversationId); - return status; + return { + unidentified: conversationIdsWithSealedSender.has(conversationId), + ...(isProtoBinaryEncodingEnabled() + ? { + destinationServiceId: null, + destinationServiceIdBinary: serviceId + ? toServiceIdObject(serviceId).getServiceIdBinary() + : null, + } + : { + destinationServiceId: serviceId ?? null, + destinationServiceIdBinary: null, + }), + destinationPniIdentityKey, + }; }, { concurrency: 10 } ); } - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.sent = sentMessage; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = MessageSender.padSyncMessage({ + content: { + sent: { + timestamp: BigInt(timestamp), + destinationE164: destinationE164 ?? null, + ...(isProtoBinaryEncodingEnabled() + ? { + destinationServiceId: null, + destinationServiceIdBinary: destinationServiceId + ? toServiceIdObject(destinationServiceId).getServiceIdBinary() + : null, + } + : { + destinationServiceId: destinationServiceId ?? null, + destinationServiceIdBinary: null, + }), + expirationStartTimestamp: expirationStartTimestamp + ? BigInt(expirationStartTimestamp) + : null, + storyMessage: storyMessage ?? null, + storyMessageRecipients: + storyMessageRecipients?.map( + ( + recipient + ): Proto.SyncMessage.Sent.StoryMessageRecipient.Params => { + return { + ...(isProtoBinaryEncodingEnabled() + ? { + destinationServiceId: null, + destinationServiceIdBinary: + recipient.destinationServiceId + ? toServiceIdObject( + recipient.destinationServiceId + ).getServiceIdBinary() + : null, + } + : { + destinationServiceId: + recipient.destinationServiceId ?? null, + destinationServiceIdBinary: null, + }), + isAllowedToReply: recipient.isAllowedToReply, + distributionListIds: recipient.distributionListIds, + }; + } + ) ?? null, + unidentifiedStatus, + editMessage, + message, + isRecipientUpdate: Boolean(isUpdate), + }, + }, + }); return this.sendIndividualProto({ serviceId: myAci, - proto: contentMessage, + proto: { + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }, timestamp, contentHint: ContentHint.Resendable, options, @@ -1601,19 +1649,28 @@ export class MessageSender { static getRequestBlockSyncMessage(): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const request = new Proto.SyncMessage.Request(); - request.type = Proto.SyncMessage.Request.Type.BLOCKED; - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.request = request; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const request: Proto.SyncMessage.Request.Params = { + type: Proto.SyncMessage.Request.Type.BLOCKED, + }; + + const syncMessage = this.padSyncMessage({ + content: { + request, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'blockSyncRequest', urgent: false, @@ -1623,19 +1680,28 @@ export class MessageSender { static getRequestConfigurationSyncMessage(): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const request = new Proto.SyncMessage.Request(); - request.type = Proto.SyncMessage.Request.Type.CONFIGURATION; - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.request = request; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const request: Proto.SyncMessage.Request.Params = { + type: Proto.SyncMessage.Request.Type.CONFIGURATION, + }; + + const syncMessage = this.padSyncMessage({ + content: { + request, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'configurationSyncRequest', urgent: false, @@ -1645,19 +1711,28 @@ export class MessageSender { static getRequestContactSyncMessage(): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const request = new Proto.SyncMessage.Request(); - request.type = Proto.SyncMessage.Request.Type.CONTACTS; - const syncMessage = this.createSyncMessage(); - syncMessage.request = request; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const request: Proto.SyncMessage.Request.Params = { + type: Proto.SyncMessage.Request.Type.CONTACTS, + }; + + const syncMessage = this.padSyncMessage({ + content: { + request, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'contactSyncRequest', urgent: true, @@ -1667,20 +1742,28 @@ export class MessageSender { static getFetchManifestSyncMessage(): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const fetchLatest = new Proto.SyncMessage.FetchLatest(); - fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST; + const fetchLatest: Proto.SyncMessage.FetchLatest.Params = { + type: Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST, + }; - const syncMessage = this.createSyncMessage(); - syncMessage.fetchLatest = fetchLatest; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = this.padSyncMessage({ + content: { + fetchLatest, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'fetchLatestManifestSync', urgent: false, @@ -1690,20 +1773,28 @@ export class MessageSender { static getFetchLocalProfileSyncMessage(): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const fetchLatest = new Proto.SyncMessage.FetchLatest(); - fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.LOCAL_PROFILE; + const fetchLatest: Proto.SyncMessage.FetchLatest.Params = { + type: Proto.SyncMessage.FetchLatest.Type.LOCAL_PROFILE, + }; - const syncMessage = this.createSyncMessage(); - syncMessage.fetchLatest = fetchLatest; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = this.padSyncMessage({ + content: { + fetchLatest, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'fetchLocalProfileSync', urgent: false, @@ -1713,20 +1804,28 @@ export class MessageSender { static getRequestKeySyncMessage(): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const request = new Proto.SyncMessage.Request(); - request.type = Proto.SyncMessage.Request.Type.KEYS; + const request: Proto.SyncMessage.Request.Params = { + type: Proto.SyncMessage.Request.Type.KEYS, + }; - const syncMessage = this.createSyncMessage(); - syncMessage.request = request; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = this.padSyncMessage({ + content: { + request, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'keySyncRequest', urgent: true, @@ -1738,7 +1837,17 @@ export class MessageSender { ): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const deleteForMe = new Proto.SyncMessage.DeleteForMe(); + const deleteForMe = { + messageDeletes: + new Array(), + conversationDeletes: + new Array(), + localOnlyConversationDeletes: + new Array(), + attachmentDeletes: + new Array(), + } satisfies Proto.SyncMessage.DeleteForMe.Params; + const messageDeletes: Map< string, Array @@ -1762,10 +1871,9 @@ export class MessageSender { const mostRecentMessages = item.mostRecentMessages.map(toAddressableMessage); const mostRecentNonExpiringMessages = - item.mostRecentNonExpiringMessages?.map(toAddressableMessage); + item.mostRecentNonExpiringMessages?.map(toAddressableMessage) ?? null; const conversation = toConversationIdentifier(item.conversation); - deleteForMe.conversationDeletes = deleteForMe.conversationDeletes || []; deleteForMe.conversationDeletes.push({ conversation, isFullDelete: true, @@ -1775,8 +1883,6 @@ export class MessageSender { } else if (item.type === 'delete-local-conversation') { const conversation = toConversationIdentifier(item.conversation); - deleteForMe.localOnlyConversationDeletes = - deleteForMe.localOnlyConversationDeletes || []; deleteForMe.localOnlyConversationDeletes.push({ conversation, }); @@ -1784,19 +1890,18 @@ export class MessageSender { const conversation = toConversationIdentifier(item.conversation); const targetMessage = toAddressableMessage(item.message); - deleteForMe.attachmentDeletes = deleteForMe.attachmentDeletes || []; deleteForMe.attachmentDeletes.push({ conversation, targetMessage, clientUuid: - item.clientUuid == null ? undefined : uuidToBytes(item.clientUuid), + item.clientUuid == null ? null : uuidToBytes(item.clientUuid), fallbackDigest: item.fallbackDigest == null - ? undefined + ? null : Bytes.fromBase64(item.fallbackDigest), fallbackPlaintextHash: item.fallbackPlaintextHash == null - ? undefined + ? null : Bytes.fromHex(item.fallbackPlaintextHash), }); } else { @@ -1819,7 +1924,6 @@ export class MessageSender { ); } - deleteForMe.messageDeletes = deleteForMe.messageDeletes || []; deleteForMe.messageDeletes.push({ messages, conversation, @@ -1827,17 +1931,24 @@ export class MessageSender { } } - const syncMessage = this.createSyncMessage(); - syncMessage.deleteForMe = deleteForMe; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = this.padSyncMessage({ + content: { + deleteForMe, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'deleteForMeSync', urgent: false, @@ -1850,21 +1961,27 @@ export class MessageSender { ): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const syncMessage = this.createSyncMessage(); - syncMessage.attachmentBackfillRequest = { - targetMessage: toAddressableMessage(targetMessage), - targetConversation: toConversationIdentifier(targetConversation), - }; - - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = this.padSyncMessage({ + content: { + attachmentBackfillRequest: { + targetMessage: toAddressableMessage(targetMessage), + targetConversation: toConversationIdentifier(targetConversation), + }, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'attachmentBackfillRequestSync', urgent: false, @@ -1875,25 +1992,31 @@ export class MessageSender { latestCall: CallHistoryDetails ): SingleProtoJobData { const ourAci = itemStorage.user.getCheckedAci(); - const callLogEvent = new Proto.SyncMessage.CallLogEvent({ + const callLogEvent: Proto.SyncMessage.CallLogEvent.Params = { type: Proto.SyncMessage.CallLogEvent.Type.CLEAR, - timestamp: Long.fromNumber(latestCall.timestamp), - peerId: getBytesForPeerId(latestCall), + timestamp: BigInt(latestCall.timestamp), + conversationId: getBytesForPeerId(latestCall), callId: getCallIdForProto(latestCall), + }; + + const syncMessage = MessageSender.padSyncMessage({ + content: { + callLogEvent, + }, }); - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.callLogEvent = callLogEvent; - - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; - return { contentHint: ContentHint.Resendable, serviceId: ourAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'callLogEventSync', urgent: false, @@ -1918,18 +2041,24 @@ export class MessageSender { status, }); - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.callEvent = callEvent; - - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = MessageSender.padSyncMessage({ + content: { + callEvent, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: ourAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'callLogEventSync', urgent: false, @@ -1939,40 +2068,44 @@ export class MessageSender { async syncReadMessages( reads: ReadonlyArray<{ senderAci?: AciString; - senderE164?: string; timestamp: number; }>, options?: Readonly ): Promise { const myAci = itemStorage.user.getCheckedAci(); - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.read = []; + const read = new Array(); for (const r of reads) { - const proto = new Proto.SyncMessage.Read( - isProtoBinaryEncodingEnabled() - ? { - senderAci: null, - senderAciBinary: r.senderAci - ? toAciObject(r.senderAci).getRawUuidBytes() - : null, - timestamp: Long.fromNumber(r.timestamp), - } - : { - senderAci: r.senderAci, - senderAciBinary: null, - timestamp: Long.fromNumber(r.timestamp), - } - ); - - syncMessage.read.push(proto); + if (isProtoBinaryEncodingEnabled()) { + read.push({ + timestamp: BigInt(r.timestamp), + senderAci: null, + senderAciBinary: r.senderAci + ? toAciObject(r.senderAci).getRawUuidBytes() + : null, + }); + } else { + read.push({ + timestamp: BigInt(r.timestamp), + senderAci: r.senderAci ?? null, + senderAciBinary: null, + }); + } } - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + + const syncMessage = MessageSender.padSyncMessage({ + read, + }); return this.sendIndividualProto({ serviceId: myAci, - proto: contentMessage, + proto: { + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }, timestamp: Date.now(), contentHint: ContentHint.Resendable, options, @@ -1983,38 +2116,44 @@ export class MessageSender { async syncView( views: ReadonlyArray<{ senderAci?: AciString; - senderE164?: string; timestamp: number; }>, options?: SendOptionsType ): Promise { const myAci = itemStorage.user.getCheckedAci(); - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.viewed = views.map( - view => - new Proto.SyncMessage.Viewed( - isProtoBinaryEncodingEnabled() - ? { - senderAci: null, - senderAciBinary: view.senderAci - ? toAciObject(view.senderAci).getRawUuidBytes() - : null, - timestamp: Long.fromNumber(view.timestamp), - } - : { - senderAci: view.senderAci, - senderAciBinary: null, - timestamp: Long.fromNumber(view.timestamp), - } - ) + const viewed = views.map( + ({ senderAci, timestamp }): Proto.SyncMessage.Viewed.Params => { + if (isProtoBinaryEncodingEnabled()) { + return { + timestamp: BigInt(timestamp), + senderAci: null, + senderAciBinary: senderAci + ? toAciObject(senderAci).getRawUuidBytes() + : null, + }; + } + return { + timestamp: BigInt(timestamp), + senderAci: senderAci ?? null, + senderAciBinary: null, + }; + } ); - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + + const syncMessage = MessageSender.padSyncMessage({ + viewed, + }); return this.sendIndividualProto({ serviceId: myAci, - proto: contentMessage, + proto: { + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }, timestamp: Date.now(), contentHint: ContentHint.Resendable, options, @@ -2043,23 +2182,32 @@ export class MessageSender { const myAci = itemStorage.user.getCheckedAci(); - const syncMessage = MessageSender.createSyncMessage(); - - const viewOnceOpen = new Proto.SyncMessage.ViewOnceOpen(); + const viewOnceOpen: Proto.SyncMessage.ViewOnceOpen.Params = { + timestamp: BigInt(timestamp), + senderAci: null, + senderAciBinary: null, + }; if (isProtoBinaryEncodingEnabled()) { viewOnceOpen.senderAciBinary = toAciObject(senderAci).getRawUuidBytes(); } else { viewOnceOpen.senderAci = senderAci; } - viewOnceOpen.timestamp = Long.fromNumber(timestamp); - syncMessage.viewOnceOpen = viewOnceOpen; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = MessageSender.padSyncMessage({ + content: { + viewOnceOpen, + }, + }); return this.sendIndividualProto({ serviceId: myAci, - proto: contentMessage, + proto: { + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }, timestamp: Date.now(), contentHint: ContentHint.Resendable, options, @@ -2076,10 +2224,12 @@ export class MessageSender { ): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const syncMessage = MessageSender.createSyncMessage(); - - const blocked = new Proto.SyncMessage.Blocked(); - blocked.numbers = options.e164s; + const blocked: Proto.SyncMessage.Blocked.Params = { + numbers: options.e164s, + acisBinary: null, + acis: null, + groupIds: options.groupIds, + }; if (isProtoBinaryEncodingEnabled()) { blocked.acisBinary = options.acis.map(aci => toAciObject(aci).getRawUuidBytes() @@ -2087,18 +2237,25 @@ export class MessageSender { } else { blocked.acis = options.acis; } - blocked.groupIds = options.groupIds; - syncMessage.blocked = blocked; - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = MessageSender.padSyncMessage({ + content: { + blocked, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'blockSync', urgent: false, @@ -2114,33 +2271,41 @@ export class MessageSender { ): SingleProtoJobData { const myAci = itemStorage.user.getCheckedAci(); - const syncMessage = MessageSender.createSyncMessage(); + const messageRequestResponse: Proto.SyncMessage.MessageRequestResponse.Params = + { + type: options.type, + groupId: options.groupId ? options.groupId : null, + threadAciBinary: null, + threadAci: null, + }; - const response = new Proto.SyncMessage.MessageRequestResponse(); if (options.threadAci !== undefined) { if (isProtoBinaryEncodingEnabled()) { - response.threadAciBinary = toAciObject( + messageRequestResponse.threadAciBinary = toAciObject( options.threadAci ).getRawUuidBytes(); } else { - response.threadAci = options.threadAci; + messageRequestResponse.threadAci = options.threadAci; } } - if (options.groupId) { - response.groupId = options.groupId; - } - response.type = options.type; - syncMessage.messageRequestResponse = response; - - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = MessageSender.padSyncMessage({ + content: { + messageRequestResponse, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'messageRequestSync', urgent: false, @@ -2157,29 +2322,34 @@ export class MessageSender { const myAci = itemStorage.user.getCheckedAci(); const ENUM = Proto.SyncMessage.StickerPackOperation.Type; - const packOperations = operations.map(item => { - const { packId, packKey, installed } = item; + const stickerPackOperation = operations.map( + (item): Proto.SyncMessage.StickerPackOperation.Params => { + const { packId, packKey, installed } = item; - const operation = new Proto.SyncMessage.StickerPackOperation(); - operation.packId = Bytes.fromHex(packId); - operation.packKey = Bytes.fromBase64(packKey); - operation.type = installed ? ENUM.INSTALL : ENUM.REMOVE; + return { + packId: Bytes.fromHex(packId), + packKey: Bytes.fromBase64(packKey), + type: installed ? ENUM.INSTALL : ENUM.REMOVE, + }; + } + ); - return operation; + const syncMessage = MessageSender.padSyncMessage({ + stickerPackOperation, }); - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.stickerPackOperation = packOperations; - - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; - return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'stickerPackSync', urgent: false, @@ -2198,10 +2368,13 @@ export class MessageSender { throw new Error('syncVerification: Neither e164 nor UUID were provided'); } - const padding = MessageSender.getRandomPadding(); - - const verified = new Proto.Verified(); - verified.state = state; + const verified: Proto.Verified.Params = { + state, + identityKey, + nullMessage: MessageSender.getRandomPadding(), + destinationAci: null, + destinationAciBinary: null, + }; if (destinationAci) { if (isProtoBinaryEncodingEnabled()) { verified.destinationAciBinary = @@ -2210,21 +2383,25 @@ export class MessageSender { verified.destinationAci = destinationAci; } } - verified.identityKey = identityKey; - verified.nullMessage = padding; - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.verified = verified; - - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = MessageSender.padSyncMessage({ + content: { + verified, + }, + }); return { contentHint: ContentHint.Resendable, serviceId: myAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }) ), type: 'verificationSync', urgent: false, @@ -2235,15 +2412,20 @@ export class MessageSender { async sendCallingMessage( serviceId: ServiceIdString, - callingMessage: Readonly, + callMessage: Readonly, timestamp: number, urgent: boolean, options?: Readonly ): Promise { const recipients = [serviceId]; - const contentMessage = new Proto.Content(); - contentMessage.callMessage = callingMessage; + const contentMessage: Proto.Content.Params = { + content: { + callMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }; const conversation = window.ConversationController.get(serviceId); @@ -2321,14 +2503,18 @@ export class MessageSender { }>): Promise { const timestamp = Date.now(); - const receiptMessage = new Proto.ReceiptMessage(); - receiptMessage.type = type; - receiptMessage.timestamp = timestamps.map(receiptTimestamp => - Long.fromNumber(receiptTimestamp) - ); + const receiptMessage: Proto.ReceiptMessage.Params = { + type, + timestamp: timestamps.map(receiptTimestamp => BigInt(receiptTimestamp)), + }; - const contentMessage = new Proto.Content(); - contentMessage.receiptMessage = receiptMessage; + const contentMessage = { + content: { + receiptMessage, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }; if (isDirectConversation) { const conversation = window.ConversationController.get(senderAci); @@ -2354,14 +2540,16 @@ export class MessageSender { options: Readonly<{ padding?: Uint8Array; }> = {} - ): Proto.Content { - const nullMessage = new Proto.NullMessage(); - nullMessage.padding = options.padding || MessageSender.getRandomPadding(); - - const contentMessage = new Proto.Content(); - contentMessage.nullMessage = nullMessage; - - return contentMessage; + ): Proto.Content.Params { + return { + content: { + nullMessage: { + padding: options.padding || MessageSender.getRandomPadding(), + }, + }, + pniSignatureMessage: null, + senderKeyDistributionMessage: null, + }; } // Group sends @@ -2455,7 +2643,7 @@ export class MessageSender { contentHint: number; groupId: string | undefined; options?: SendOptionsType; - proto: Proto.Content; + proto: Proto.Content.Params; recipients: ReadonlyArray; sendLogCallback?: SendLogCallbackType; story?: boolean; @@ -2467,12 +2655,12 @@ export class MessageSender { const serviceIds = recipients.filter(id => id !== myE164 && id !== myAci); if (serviceIds.length === 0) { - const dataMessage = proto.dataMessage - ? Proto.DataMessage.encode(proto.dataMessage).finish() + const dataMessage = proto.content?.dataMessage + ? Proto.DataMessage.encode(proto.content.dataMessage) : undefined; - const editMessage = proto.editMessage - ? Proto.EditMessage.encode(proto.editMessage).finish() + const editMessage = proto.content?.editMessage + ? Proto.EditMessage.encode(proto.content.editMessage) : undefined; return Promise.resolve({ @@ -2519,7 +2707,7 @@ export class MessageSender { throwIfNotInDatabase, timestamp, }: { throwIfNotInDatabase?: boolean; timestamp: number } - ): Promise { + ): Promise { const ourAci = itemStorage.user.getCheckedAci(); const ourDeviceId = parseIntOrThrow( itemStorage.user.getDeviceId(), @@ -2561,11 +2749,11 @@ export class MessageSender { log.info( `getSenderKeyDistributionMessage: Building ${distributionId} with timestamp ${timestamp}` ); - const contentMessage = new Proto.Content(); - contentMessage.senderKeyDistributionMessage = - senderKeyDistributionMessage.serialize(); - - return contentMessage; + return { + content: null, + pniSignatureMessage: null, + senderKeyDistributionMessage: senderKeyDistributionMessage.serialize(), + }; } // The one group send exception - a message that should never be sent via sender key @@ -2602,7 +2790,7 @@ export class MessageSender { serviceIds.length > 1 ? this.makeSendLogCallback({ contentHint: contentHint ?? ContentHint.Implicit, - proto: Proto.Content.encode(contentMessage).finish(), + proto: Proto.Content.encode(contentMessage), sendType: 'senderKeyDistributionMessage', timestamp, urgent, @@ -2628,61 +2816,127 @@ export const messageSender = new MessageSender(); // Helpers -function toAddressableMessage(message: AddressableMessage) { - const targetMessage = new Proto.AddressableMessage(); - targetMessage.sentTimestamp = Long.fromNumber(message.sentAt); +function toAddressableMessage( + message: AddressableMessage +): Proto.AddressableMessage.Params { + const sentTimestamp = BigInt(message.sentAt); + let author: Proto.AddressableMessage.Params['author']; if (message.type === 'aci') { if (isProtoBinaryEncodingEnabled()) { - targetMessage.authorServiceIdBinary = toAciObject( - message.authorAci - ).getServiceIdBinary(); + author = { + authorServiceIdBinary: toAciObject( + message.authorAci + ).getServiceIdBinary(), + }; } else { - targetMessage.authorServiceId = message.authorAci; + author = { + authorServiceId: message.authorAci, + }; } } else if (message.type === 'e164') { - targetMessage.authorE164 = message.authorE164; + author = { + authorE164: message.authorE164, + }; } else if (message.type === 'pni') { if (isProtoBinaryEncodingEnabled()) { - targetMessage.authorServiceIdBinary = toPniObject( - message.authorPni - ).getServiceIdBinary(); + author = { + authorServiceIdBinary: toPniObject( + message.authorPni + ).getServiceIdBinary(), + }; } else { - targetMessage.authorServiceId = message.authorPni; + author = { + authorServiceId: message.authorPni, + }; } } else { throw missingCaseError(message); } - return targetMessage; + return { + sentTimestamp, + author, + }; } -function toConversationIdentifier(conversation: ConversationIdentifier) { - const targetConversation = new Proto.ConversationIdentifier(); - +function toConversationIdentifier( + conversation: ConversationIdentifier +): Proto.ConversationIdentifier.Params { if (conversation.type === 'aci') { if (isProtoBinaryEncodingEnabled()) { - targetConversation.threadServiceIdBinary = toAciObject( - conversation.aci - ).getServiceIdBinary(); - } else { - targetConversation.threadServiceId = conversation.aci; + return { + identifier: { + threadServiceIdBinary: toAciObject( + conversation.aci + ).getServiceIdBinary(), + }, + }; } - } else if (conversation.type === 'pni') { + return { + identifier: { + threadServiceId: conversation.aci, + }, + }; + } + if (conversation.type === 'pni') { if (isProtoBinaryEncodingEnabled()) { - targetConversation.threadServiceIdBinary = toPniObject( - conversation.pni - ).getServiceIdBinary(); - } else { - targetConversation.threadServiceId = conversation.pni; + return { + identifier: { + threadServiceIdBinary: toPniObject( + conversation.pni + ).getServiceIdBinary(), + }, + }; } - } else if (conversation.type === 'group') { - targetConversation.threadGroupId = Bytes.fromBase64(conversation.groupId); - } else if (conversation.type === 'e164') { - targetConversation.threadE164 = conversation.e164; + return { + identifier: { + threadServiceId: conversation.pni, + }, + }; + } + if (conversation.type === 'group') { + return { + identifier: { + threadGroupId: Bytes.fromBase64(conversation.groupId), + }, + }; + } + if (conversation.type === 'e164') { + return { + identifier: { + threadE164: conversation.e164, + }, + }; + } + throw missingCaseError(conversation); +} + +function toBodyRange(bodyRange: RawBodyRange): Proto.BodyRange.Params { + const { start, length } = bodyRange; + + let associatedValue: Proto.BodyRange.Params['associatedValue']; + if (BodyRange.isMention(bodyRange)) { + if (isProtoBinaryEncodingEnabled()) { + associatedValue = { + mentionAciBinary: toAciObject(bodyRange.mentionAci).getRawUuidBytes(), + }; + } else { + associatedValue = { + mentionAci: bodyRange.mentionAci, + }; + } + } else if (BodyRange.isFormatting(bodyRange)) { + associatedValue = { + style: bodyRange.style, + }; } else { - throw missingCaseError(conversation); + throw missingCaseError(bodyRange); } - return targetConversation; + return { + start, + length, + associatedValue, + }; } diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 52945c3a2f..6ae028d15f 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -16,6 +16,7 @@ import type { MIMEType } from '../types/MIME.std.js'; import type { DurationInSeconds } from '../util/durations/index.std.js'; import type { AnyPaymentEvent } from '../types/Payment.std.js'; import type { RawBodyRange } from '../types/BodyRange.std.js'; +import type { StoryMessageRecipientsType } from '../types/Stories.std.js'; export { IdentityKeyType, @@ -158,7 +159,7 @@ export type ProcessedAvatar = { isProfile: boolean; }; -export type ProcessedContact = Omit & { +export type ProcessedContact = Omit & { avatar?: ProcessedAvatar; }; @@ -219,7 +220,7 @@ export type ProcessedAdminDelete = Readonly<{ export type ProcessedBodyRange = RawBodyRange; -export type ProcessedGroupCallUpdate = Proto.DataMessage.IGroupCallUpdate; +export type ProcessedGroupCallUpdate = Proto.DataMessage.GroupCallUpdate; export type ProcessedGiftBadge = { expiration: number; @@ -273,37 +274,26 @@ export type ProcessedDataMessage = { canReplyToStory?: boolean; }; -export type ProcessedUnidentifiedDeliveryStatus = Omit< - Proto.SyncMessage.Sent.IUnidentifiedDeliveryStatus, - 'destinationAci' | 'destinationPni' -> & { +export type ProcessedUnidentifiedDeliveryStatus = Readonly<{ destinationServiceId?: ServiceIdString; isAllowedToReplyToStory?: boolean; -}; - -export type ProcessedStoryMessageRecipient = Omit< - Proto.SyncMessage.Sent.IStoryMessageRecipient, - 'destinationAci' | 'destinationPni' -> & { - destinationServiceId?: ServiceIdString; -}; + destinationPniIdentityKey?: Uint8Array; + unidentified?: boolean; +}>; export type ProcessedSent = Omit< - Proto.SyncMessage.ISent, + Proto.SyncMessage.Sent, + | '$unknown' | 'destinationId' | 'unidentifiedStatus' | 'storyMessageRecipients' - | 'destinationAci' - | 'destinationPni' + | 'destinationServiceId' + | 'destinationServiceIdBinary' > & { destinationId?: string; destinationServiceId?: ServiceIdString; unidentifiedStatus?: Array; - storyMessageRecipients?: Array; -}; - -export type ProcessedSyncMessage = Omit & { - sent?: ProcessedSent; + storyMessageRecipients?: StoryMessageRecipientsType; }; export type CustomError = Error & { diff --git a/ts/textsecure/WebAPI.preload.ts b/ts/textsecure/WebAPI.preload.ts index d82e636ced..90557a3116 100644 --- a/ts/textsecure/WebAPI.preload.ts +++ b/ts/textsecure/WebAPI.preload.ts @@ -3427,9 +3427,9 @@ export async function callLinkCreateAuth( } export async function submitCallQualitySurvey( - survey: Proto.ISubmitCallQualitySurveyRequest + survey: Proto.SubmitCallQualitySurveyRequest.Params ): Promise { - const data = Proto.SubmitCallQualitySurveyRequest.encode(survey).finish(); + const data = Proto.SubmitCallQualitySurveyRequest.encode(survey); await _ajax({ call: 'callQualitySurvey', contentType: 'application/octet-stream', @@ -4327,7 +4327,7 @@ export async function getGroupCredentials({ export async function getExternalGroupCredential( options: GroupCredentialsType -): Promise { +): Promise { const basicAuth = generateGroupAuth( options.groupPublicParamsHex, options.authCredentialPresentationHex @@ -4346,7 +4346,7 @@ export async function getExternalGroupCredential( return Proto.ExternalGroupCredential.decode(response); } -function verifyAttributes(attributes: Proto.IAvatarUploadAttributes) { +function verifyAttributes(attributes: Proto.AvatarUploadAttributes.Params) { const { key, credential, acl, algorithm, date, policy, signature } = attributes; @@ -4581,14 +4581,14 @@ export function confirmPaypalBoostPayment( } export async function createGroup( - group: Proto.IGroup, + group: Proto.Group.Params, options: GroupCredentialsType -): Promise { +): Promise { const basicAuth = generateGroupAuth( options.groupPublicParamsHex, options.authCredentialPresentationHex ); - const data = Proto.Group.encode(group).finish(); + const data = Proto.Group.encode(group); const response = await _ajax({ basicAuth, @@ -4606,7 +4606,7 @@ export async function createGroup( export async function getGroup( options: GroupCredentialsType -): Promise { +): Promise { const basicAuth = generateGroupAuth( options.groupPublicParamsHex, options.authCredentialPresentationHex @@ -4655,15 +4655,15 @@ export async function getGroupFromLink( } export async function modifyGroup( - changes: Proto.GroupChange.IActions, + changes: Proto.GroupChange.Actions.Params, options: GroupCredentialsType, inviteLinkBase64?: string -): Promise { +): Promise { const basicAuth = generateGroupAuth( options.groupPublicParamsHex, options.authCredentialPresentationHex ); - const data = Proto.GroupChange.Actions.encode(changes).finish(); + const data = Proto.GroupChange.Actions.encode(changes); const safeInviteLinkPassword = inviteLinkBase64 ? toWebSafeBase64(inviteLinkBase64) : undefined; diff --git a/ts/textsecure/messageReceiverEvents.std.ts b/ts/textsecure/messageReceiverEvents.std.ts index e7b8c70aea..c346c1d509 100644 --- a/ts/textsecure/messageReceiverEvents.std.ts +++ b/ts/textsecure/messageReceiverEvents.std.ts @@ -12,6 +12,7 @@ import { isPniString, } from '../types/ServiceId.std.js'; import type { StoryDistributionIdString } from '../types/StoryDistributionId.std.js'; +import type { StoryMessageRecipientsType } from '../types/Stories.std.js'; import type { ProcessedEnvelope, ProcessedDataMessage, @@ -32,7 +33,7 @@ export class EmptyEvent extends Event { } export type TypingEventData = Readonly<{ - typingMessage: Proto.ITypingMessage; + typingMessage: Proto.TypingMessage; timestamp: number; started: boolean; stopped: boolean; @@ -290,7 +291,7 @@ export class ViewEvent extends ConfirmableEvent { export class ConfigurationEvent extends ConfirmableEvent { constructor( - public readonly configuration: Proto.SyncMessage.IConfiguration, + public readonly configuration: Proto.SyncMessage.Configuration, confirm: ConfirmCallback ) { super('configuration', confirm); @@ -325,7 +326,7 @@ export type MessageRequestResponseOptions = { envelopeId: string; threadE164?: string; threadAci?: AciString; - messageRequestResponseType: Proto.SyncMessage.IMessageRequestResponse['type']; + messageRequestResponseType: Proto.SyncMessage.MessageRequestResponse.Type; groupId?: string; groupV2Id?: string; receivedAtCounter: number; @@ -378,7 +379,7 @@ export class MessageRequestResponseEvent extends ConfirmableEvent { export class FetchLatestEvent extends ConfirmableEvent { constructor( - public readonly eventType: Proto.SyncMessage.IFetchLatest['type'], + public readonly eventType: Proto.SyncMessage.FetchLatest['type'], confirm: ConfirmCallback ) { super('fetchLatest', confirm); @@ -641,7 +642,7 @@ export class CallLogEventSyncEvent extends ConfirmableEvent { export type StoryRecipientUpdateData = Readonly<{ destinationServiceId: ServiceIdString; - storyMessageRecipients: Array; + storyMessageRecipients: StoryMessageRecipientsType; timestamp: number; }>; diff --git a/ts/textsecure/processDataMessage.preload.ts b/ts/textsecure/processDataMessage.preload.ts index 5396233071..b79fb521cc 100644 --- a/ts/textsecure/processDataMessage.preload.ts +++ b/ts/textsecure/processDataMessage.preload.ts @@ -1,17 +1,17 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import Long from 'long'; import { ReceiptCredentialPresentation } from '@signalapp/libsignal-client/zkgroup.js'; import lodash from 'lodash'; import { assertDev, strictAssert } from '../util/assert.std.js'; -import { dropNull, shallowDropNull } from '../util/dropNull.std.js'; +import { dropNull } from '../util/dropNull.std.js'; import { fromAciUuidBytes, fromAciUuidBytesOrString, } from '../util/ServiceId.node.js'; import { getTimestampFromLong } from '../util/timestampLongUtils.std.js'; +import { isKnownProtoEnumMember } from '../util/isKnownProtoEnumMember.std.js'; import { SignalService as Proto } from '../protobuf/index.std.js'; import { deriveGroupFields } from '../groups.preload.js'; import * as Bytes from '../Bytes.std.js'; @@ -36,6 +36,7 @@ import type { ProcessedUnpinMessage, } from './Types.d.ts'; import { GiftBadgeStates } from '../types/GiftBadgeStates.std.js'; +import type { RawBodyRange } from '../types/BodyRange.std.js'; import { APPLICATION_OCTET_STREAM, stringToMIMEType, @@ -54,6 +55,8 @@ import { partitionBodyAndNormalAttachments } from '../util/Attachment.std.js'; import { isNotNil } from '../util/isNotNil.std.js'; import { createLogger } from '../logging/log.std.js'; +import { toNumber } from '../util/toNumber.std.js'; + const { isNumber } = lodash; const log = createLogger('processDataMessage'); @@ -62,24 +65,22 @@ const FLAGS = Proto.DataMessage.Flags; export const ATTACHMENT_MAX = 32; export function processAttachment( - attachment: Proto.IAttachmentPointer + attachment: Proto.AttachmentPointer ): ProcessedAttachment; export function processAttachment( - attachment?: Proto.IAttachmentPointer | null + attachment?: Proto.AttachmentPointer | null ): ProcessedAttachment | undefined; export function processAttachment( - attachment?: Proto.IAttachmentPointer | null + attachment?: Proto.AttachmentPointer | null ): ProcessedAttachment | undefined { - const attachmentWithoutNulls = shallowDropNull(attachment); - if (!attachmentWithoutNulls) { + if (attachment == null) { return undefined; } const { - cdnId, - cdnKey, cdnNumber, + attachmentIdentifier, clientUuid, key, size, @@ -93,15 +94,14 @@ export function processAttachment( height, caption, blurHash, - } = attachmentWithoutNulls; - - const hasCdnId = Long.isLong(cdnId) ? !cdnId.isZero() : Boolean(cdnId); + } = attachment; if (!isNumber(size)) { throw new Error('Missing size on incoming attachment!'); } - let uploadTimestamp = attachmentWithoutNulls.uploadTimestamp?.toNumber(); + let uploadTimestamp: number | undefined = + toNumber(attachment.uploadTimestamp) ?? 0; // Make sure uploadTimestamp is not set to an obviously wrong future value (we use // uploadTimestamp to determine whether to re-use CDN pointers) @@ -111,17 +111,20 @@ export function processAttachment( } return { - cdnKey, - cdnNumber, - chunkSize, - fileName, - flags, - width, - height, - caption, - blurHash, + cdnKey: attachmentIdentifier?.cdnKey, + cdnNumber: cdnNumber ?? 0, + chunkSize: chunkSize ?? 0, + fileName: fileName ?? '', + flags: flags ?? 0, + width: width ?? 0, + height: height ?? 0, + caption: caption ?? '', + blurHash: blurHash ?? '', uploadTimestamp, - cdnId: hasCdnId ? String(cdnId) : undefined, + cdnId: + attachmentIdentifier?.cdnId === 0n + ? undefined + : attachmentIdentifier?.cdnId?.toString(), clientUuid: Bytes.isNotEmpty(clientUuid) ? bytesToUuid(clientUuid) : undefined, @@ -138,7 +141,7 @@ export function processAttachment( } export function processGroupV2Context( - groupV2?: Proto.IGroupContextV2 | null + groupV2?: Proto.GroupContextV2 | null ): ProcessedGroupV2Context | undefined { if (!groupV2) { return undefined; @@ -149,7 +152,7 @@ export function processGroupV2Context( return { masterKey: Bytes.toBase64(groupV2.masterKey), - revision: dropNull(groupV2.revision), + revision: groupV2.revision ?? 0, groupChange: groupV2.groupChange ? Bytes.toBase64(groupV2.groupChange) : undefined, @@ -160,28 +163,28 @@ export function processGroupV2Context( } export function processPayment( - payment?: Proto.DataMessage.IPayment | null + payment?: Proto.DataMessage.Payment | null ): AnyPaymentEvent | undefined { if (!payment) { return undefined; } - if (payment.notification != null) { + if (payment.Item?.notification != null) { return { kind: PaymentEventKind.Notification, - note: payment.notification.note ?? null, + note: payment.Item.notification.note ?? null, }; } - if (payment.activation != null) { + if (payment.Item?.activation != null) { if ( - payment.activation.type === + payment.Item.activation.type === Proto.DataMessage.Payment.Activation.Type.REQUEST ) { return { kind: PaymentEventKind.ActivationRequest }; } if ( - payment.activation.type === + payment.Item.activation.type === Proto.DataMessage.Payment.Activation.Type.ACTIVATED ) { return { kind: PaymentEventKind.Activation }; @@ -192,7 +195,7 @@ export function processPayment( } export function processQuote( - quote?: Proto.DataMessage.IQuote | null + quote?: Proto.DataMessage.Quote | null ): ProcessedQuote | undefined { if (!quote) { return undefined; @@ -206,25 +209,29 @@ export function processQuote( ); return { - id: quote.id?.toNumber(), + id: toNumber(quote.id) ?? 0, authorAci, - text: dropNull(quote.text), + text: quote.text ?? '', attachments: (quote.attachments ?? []).slice(0, 1).map(attachment => { return { contentType: attachment.contentType ? stringToMIMEType(attachment.contentType) : APPLICATION_OCTET_STREAM, - fileName: dropNull(attachment.fileName), + fileName: attachment.fileName ?? '', thumbnail: processAttachment(attachment.thumbnail), }; }), - bodyRanges: filterAndClean(quote.bodyRanges), - type: quote.type || Proto.DataMessage.Quote.Type.NORMAL, + bodyRanges: filterAndClean( + quote.bodyRanges.map(processBodyRange).filter(isNotNil) + ), + type: isKnownProtoEnumMember(Proto.DataMessage.Quote.Type, quote.type) + ? quote.type + : Proto.DataMessage.Quote.Type.NORMAL, }; } export function processStoryContext( - storyContext?: Proto.DataMessage.IStoryContext | null + storyContext?: Proto.DataMessage.StoryContext | null ): ProcessedStoryContext | undefined { if (!storyContext) { return undefined; @@ -248,7 +255,7 @@ export function processStoryContext( } export function processContact( - contact?: ReadonlyArray | null + contact?: ReadonlyArray | null ): ReadonlyArray | undefined { if (!contact) { return undefined; @@ -276,13 +283,13 @@ function isLinkPreviewDateValid(value: unknown): value is number { ); } -function cleanLinkPreviewDate(value?: Long | null): number | undefined { - const result = value?.toNumber(); +function cleanLinkPreviewDate(value?: bigint | null): number | undefined { + const result = toNumber(value); return isLinkPreviewDateValid(result) ? result : undefined; } export function processPreview( - preview?: ReadonlyArray | null + preview?: ReadonlyArray | null ): ReadonlyArray | undefined { if (!preview) { return undefined; @@ -290,17 +297,17 @@ export function processPreview( return preview.slice(0, 1).map(item => { return { - url: dropNull(item.url), - title: dropNull(item.title), + url: item.url ?? '', + title: item.title ?? '', image: item.image ? processAttachment(item.image) : undefined, - description: dropNull(item.description), + description: item.description ?? '', date: cleanLinkPreviewDate(item.date), }; }); } export function processSticker( - sticker?: Proto.DataMessage.ISticker | null + sticker?: Proto.DataMessage.Sticker | null ): ProcessedSticker | undefined { if (!sticker) { return undefined; @@ -309,14 +316,14 @@ export function processSticker( return { packId: sticker.packId ? Bytes.toHex(sticker.packId) : undefined, packKey: sticker.packKey ? Bytes.toBase64(sticker.packKey) : undefined, - stickerId: dropNull(sticker.stickerId), - emoji: dropNull(sticker.emoji), + stickerId: sticker.stickerId ?? 0, + emoji: sticker.emoji ?? '', data: processAttachment(sticker.data), }; } export function processReaction( - reaction?: Proto.DataMessage.IReaction | null + reaction?: Proto.DataMessage.Reaction | null ): ProcessedReaction | undefined { if (!reaction) { return undefined; @@ -331,32 +338,37 @@ export function processReaction( ); return { - emoji: dropNull(reaction.emoji), + emoji: reaction.emoji ?? '', remove: Boolean(reaction.remove), targetAuthorAci, - targetTimestamp: reaction.targetSentTimestamp?.toNumber(), + targetTimestamp: toNumber(reaction.targetSentTimestamp) ?? 0, }; } export function processPinMessage( - pinMessage?: Proto.DataMessage.IPinMessage | null + pinMessage?: Proto.DataMessage.PinMessage | null ): ProcessedPinMessage | undefined { if (pinMessage == null) { return undefined; } - const targetSentTimestamp = pinMessage.targetSentTimestamp?.toNumber(); + const targetSentTimestamp = toNumber(pinMessage.targetSentTimestamp); strictAssert(targetSentTimestamp, 'Missing targetSentTimestamp'); const targetAuthorAci = fromAciUuidBytes(pinMessage.targetAuthorAciBinary); strictAssert(targetAuthorAci, 'Missing targetAuthorAciBinary'); let pinDuration: DurationInSeconds | null; - if (pinMessage.pinDurationForever) { + if (pinMessage.pinDuration?.pinDurationForever) { pinDuration = null; } else { - strictAssert(pinMessage.pinDurationSeconds, 'Missing pinDurationSeconds'); - pinDuration = DurationInSeconds.fromSeconds(pinMessage.pinDurationSeconds); + strictAssert( + pinMessage.pinDuration?.pinDurationSeconds, + 'Missing pinDurationSeconds' + ); + pinDuration = DurationInSeconds.fromSeconds( + pinMessage.pinDuration.pinDurationSeconds + ); } return { @@ -367,21 +379,21 @@ export function processPinMessage( } export function processPollCreate( - pollCreate?: Proto.DataMessage.IPollCreate | null + pollCreate?: Proto.DataMessage.PollCreate | null ): ProcessedPollCreate | undefined { if (!pollCreate) { return undefined; } return { - question: dropNull(pollCreate.question), + question: pollCreate.question ?? '', options: pollCreate.options?.filter(isNotNil) || [], allowMultiple: Boolean(pollCreate.allowMultiple), }; } export function processPollVote( - pollVote?: Proto.DataMessage.IPollVote | null + pollVote?: Proto.DataMessage.PollVote | null ): ProcessedPollVote | undefined { if (!pollVote) { return undefined; @@ -395,44 +407,44 @@ export function processPollVote( return { targetAuthorAci, - targetTimestamp: pollVote.targetSentTimestamp?.toNumber(), + targetTimestamp: toNumber(pollVote.targetSentTimestamp) ?? 0, optionIndexes: pollVote.optionIndexes?.filter(isNotNil) || [], voteCount: pollVote.voteCount || 0, }; } export function processPollTerminate( - pollTerminate?: Proto.DataMessage.IPollTerminate | null + pollTerminate?: Proto.DataMessage.PollTerminate | null ): ProcessedPollTerminate | undefined { if (!pollTerminate) { return undefined; } return { - targetTimestamp: pollTerminate.targetSentTimestamp?.toNumber(), + targetTimestamp: toNumber(pollTerminate.targetSentTimestamp) ?? 0, }; } export function processDelete( - del?: Proto.DataMessage.IDelete | null + del?: Proto.DataMessage.Delete | null ): ProcessedDelete | undefined { if (!del) { return undefined; } return { - targetSentTimestamp: del.targetSentTimestamp?.toNumber(), + targetSentTimestamp: toNumber(del.targetSentTimestamp) ?? 0, }; } export function processAdminDelete( - adminDelete?: Proto.DataMessage.IAdminDelete | null + adminDelete?: Proto.DataMessage.AdminDelete | null ): ProcessedAdminDelete | undefined { if (!adminDelete) { return undefined; } - const targetSentTimestamp = adminDelete.targetSentTimestamp?.toNumber(); + const targetSentTimestamp = toNumber(adminDelete.targetSentTimestamp); strictAssert(targetSentTimestamp, 'AdminDelete missing targetSentTimestamp'); const targetAuthorAci = fromAciUuidBytes(adminDelete.targetAuthorAciBinary); @@ -445,7 +457,7 @@ export function processAdminDelete( } export function processGiftBadge( - giftBadge: Proto.DataMessage.IGiftBadge | null | undefined + giftBadge: Proto.DataMessage.GiftBadge | null | undefined ): ProcessedGiftBadge | undefined { if ( !giftBadge || @@ -471,13 +483,13 @@ export function processGiftBadge( } export function processUnpinMessage( - unpinMessage?: Proto.DataMessage.IUnpinMessage | null + unpinMessage?: Proto.DataMessage.UnpinMessage | null ): ProcessedUnpinMessage | undefined { if (unpinMessage == null) { return undefined; } - const targetSentTimestamp = unpinMessage.targetSentTimestamp?.toNumber(); + const targetSentTimestamp = toNumber(unpinMessage.targetSentTimestamp); strictAssert(targetSentTimestamp, 'Missing targetSentTimestamp'); const targetAuthorAci = fromAciUuidBytes(unpinMessage.targetAuthorAciBinary); @@ -490,7 +502,7 @@ export function processUnpinMessage( } export function processDataMessage( - message: Proto.IDataMessage, + message: Proto.DataMessage, envelopeTimestamp: number, // Only for testing @@ -507,7 +519,7 @@ export function processDataMessage( throw new Error('Missing timestamp on dataMessage'); } - const timestamp = message.timestamp?.toNumber(); + const timestamp = toNumber(message.timestamp); if (envelopeTimestamp !== timestamp) { throw new Error( @@ -517,7 +529,7 @@ export function processDataMessage( } const processedAttachments = message.attachments - ?.map((attachment: Proto.IAttachmentPointer) => ({ + ?.map((attachment: Proto.AttachmentPointer) => ({ ...processAttachment(attachment), downloadPath: doCreateName(), })) @@ -529,7 +541,7 @@ export function processDataMessage( ); const result: ProcessedDataMessage = { - body: dropNull(message.body), + body: message.body ?? '', bodyAttachment, attachments, groupV2: processGroupV2Context(message.groupV2), @@ -546,7 +558,7 @@ export function processDataMessage( contact: processContact(message.contact), preview: processPreview(message.preview), sticker: processSticker(message.sticker), - requiredProtocolVersion: dropNull(message.requiredProtocolVersion), + requiredProtocolVersion: message.requiredProtocolVersion ?? 0, isViewOnce: Boolean(message.isViewOnce), reaction: processReaction(message.reaction), pinMessage: processPinMessage(message.pinMessage), @@ -555,7 +567,9 @@ export function processDataMessage( pollTerminate: processPollTerminate(message.pollTerminate), delete: processDelete(message.delete), adminDelete: processAdminDelete(message.adminDelete), - bodyRanges: filterAndClean(message.bodyRanges), + bodyRanges: filterAndClean( + message.bodyRanges.map(processBodyRange).filter(isNotNil) + ), groupCallUpdate: dropNull(message.groupCallUpdate), storyContext: processStoryContext(message.storyContext), giftBadge: processGiftBadge(message.giftBadge), @@ -605,3 +619,36 @@ export function processDataMessage( return result; } + +export function processBodyRange( + proto: Proto.BodyRange +): RawBodyRange | undefined { + if (proto.associatedValue == null) { + return undefined; + } + if (proto.associatedValue.style) { + return { + start: proto.start ?? 0, + length: proto.length ?? 0, + style: isKnownProtoEnumMember( + Proto.BodyRange.Style, + proto.associatedValue.style + ) + ? proto.associatedValue.style + : 0, + }; + } + + const mentionAci = fromAciUuidBytesOrString( + proto.associatedValue.mentionAciBinary, + proto.associatedValue.mentionAci, + 'BodyRange.mentionAci' + ); + strictAssert(mentionAci != null, 'Expected mentionAci'); + + return { + start: proto.start ?? 0, + length: proto.length ?? 0, + mentionAci, + }; +} diff --git a/ts/textsecure/processSyncMessage.node.ts b/ts/textsecure/processSyncMessage.node.ts index b2ef2d3054..8389ce32dc 100644 --- a/ts/textsecure/processSyncMessage.node.ts +++ b/ts/textsecure/processSyncMessage.node.ts @@ -3,8 +3,9 @@ import type { SignalService as Proto } from '../protobuf/index.std.js'; import type { ServiceIdString } from '../types/ServiceId.std.js'; +import { normalizeStoryDistributionId } from '../types/StoryDistributionId.std.js'; import { fromServiceIdBinaryOrString } from '../util/ServiceId.node.js'; -import type { ProcessedSent, ProcessedSyncMessage } from './Types.d.ts'; +import type { ProcessedSent } from './Types.d.ts'; type ProtoServiceId = Readonly<{ destinationServiceId?: string | null; @@ -33,21 +34,18 @@ function processProtoWithDestinationServiceId( }; } -function processSent( - sent?: Proto.SyncMessage.ISent | null -): ProcessedSent | undefined { - if (!sent) { - return undefined; - } - +export function processSent(sent: Proto.SyncMessage.Sent): ProcessedSent { const { destinationServiceId: rawDestinationServiceId, destinationServiceIdBinary, unidentifiedStatus, storyMessageRecipients, + $unknown, ...remaining } = sent; + void $unknown; + return { ...remaining, @@ -57,19 +55,31 @@ function processSent( 'processSent' ), unidentifiedStatus: unidentifiedStatus - ? unidentifiedStatus.map(processProtoWithDestinationServiceId) + ? unidentifiedStatus + .map(processProtoWithDestinationServiceId) + .map(({ unidentified, destinationPniIdentityKey, ...rest }) => { + return { + ...rest, + unidentified: unidentified ?? false, + destinationPniIdentityKey: destinationPniIdentityKey ?? undefined, + }; + }) : undefined, storyMessageRecipients: storyMessageRecipients - ? storyMessageRecipients.map(processProtoWithDestinationServiceId) + ? storyMessageRecipients + .map(processProtoWithDestinationServiceId) + .map(recipient => { + return { + isAllowedToReply: recipient.isAllowedToReply ?? false, + destinationServiceId: recipient.destinationServiceId, + distributionListIds: recipient.distributionListIds.map(id => { + return normalizeStoryDistributionId( + id, + 'processSent.storyMessageRecipients' + ); + }), + }; + }) : undefined, }; } - -export function processSyncMessage( - syncMessage: Proto.ISyncMessage -): ProcessedSyncMessage { - return { - ...syncMessage, - sent: processSent(syncMessage.sent), - }; -} diff --git a/ts/types/Attachment.std.ts b/ts/types/Attachment.std.ts index 57788f2563..72ca2114b1 100644 --- a/ts/types/Attachment.std.ts +++ b/ts/types/Attachment.std.ts @@ -126,10 +126,15 @@ export type AttachmentForUIType = AttachmentType & { }; }; -export type UploadedAttachmentType = Proto.IAttachmentPointer & +export type UploadedAttachmentType = Omit< + Proto.AttachmentPointer.Params, + 'attachmentIdentifier' +> & Readonly<{ // Required fields - cdnKey: string; + attachmentIdentifier: Readonly<{ + cdnKey: string; + }>; key: Uint8Array; size: number; digest: Uint8Array; diff --git a/ts/types/CallDisposition.std.ts b/ts/types/CallDisposition.std.ts index 579c26e5aa..d75f743151 100644 --- a/ts/types/CallDisposition.std.ts +++ b/ts/types/CallDisposition.std.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import { z } from 'zod'; -import Long from 'long'; import type { AciString } from './ServiceId.std.js'; import { aciSchema } from './ServiceId.std.js'; import { bytesToUuid } from '../util/uuidToBytes.std.js'; @@ -10,6 +9,8 @@ import { SignalService as Proto } from '../protobuf/index.std.js'; import * as Bytes from '../Bytes.std.js'; import { UUID_BYTE_SIZE } from './Crypto.std.js'; +import { toNumber } from '../util/toNumber.std.js'; + // These are strings (1) for the backup (2) for Storybook. export enum CallMode { Direct = 'Direct', @@ -272,13 +273,9 @@ const roomIdInBytesSchema = z .instanceof(Uint8Array) .transform(value => Bytes.toHex(value)); -const longToStringSchema = z - .instanceof(Long) - .transform(long => long.toString()); +const longToStringSchema = z.bigint().transform(big => big.toString()); -const longToNumberSchema = z - .instanceof(Long) - .transform(long => long.toNumber()); +const longToNumberSchema = z.bigint().transform(big => toNumber(big)); export const callEventNormalizeSchema = z .object({ @@ -293,13 +290,13 @@ export const callEventNormalizeSchema = z type: z .nativeEnum(Proto.SyncMessage.CallEvent.Type) .refine(val => val === Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL), - peerId: roomIdInBytesSchema, + conversationId: roomIdInBytesSchema, }), z.object({ type: z .nativeEnum(Proto.SyncMessage.CallEvent.Type) .refine(val => val !== Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL), - peerId: conversationPeerIdInBytesSchema, + conversationId: conversationPeerIdInBytesSchema, }), ]) ); diff --git a/ts/types/LinkPreview.std.ts b/ts/types/LinkPreview.std.ts index 0e825ed5e7..5272452d42 100644 --- a/ts/types/LinkPreview.std.ts +++ b/ts/types/LinkPreview.std.ts @@ -81,7 +81,7 @@ export function shouldPreviewHref(href: string): boolean { export function isValidLinkPreview( urlsInBody: Array, - preview: LinkPreviewType | Backups.ILinkPreview, + preview: LinkPreviewType | Backups.LinkPreview.Params, { isStory }: { isStory: boolean } ): boolean { const { url } = preview; diff --git a/ts/types/Stickers.preload.ts b/ts/types/Stickers.preload.ts index df40f144fe..c80dd3910d 100644 --- a/ts/types/Stickers.preload.ts +++ b/ts/types/Stickers.preload.ts @@ -463,7 +463,7 @@ function decryptSticker(packKey: string, ciphertext: Uint8Array): Uint8Array { async function downloadSticker( packId: string, packKey: string, - proto: Proto.StickerPack.ISticker, + proto: Proto.StickerPack.Sticker.Params, { ephemeral }: { ephemeral?: boolean } = {} ): Promise> { const { id, emoji } = proto; @@ -628,7 +628,7 @@ export async function downloadEphemeralPack( stickerPackAdded(pack); const downloadStickerJob = async ( - stickerProto: Proto.StickerPack.ISticker + stickerProto: Proto.StickerPack.Sticker.Params ): Promise => { let stickerInfo; try { @@ -793,10 +793,10 @@ async function doDownloadStickerPack( return; } - let coverProto: Proto.StickerPack.ISticker | undefined; + let coverProto: Proto.StickerPack.Sticker.Params | undefined; let coverStickerId: number | undefined; let coverIncludedInList = false; - let nonCoverStickers: Array = []; + let nonCoverStickers: Array = []; try { // Synchronous placeholder to help with race conditions @@ -891,7 +891,7 @@ async function doDownloadStickerPack( // and we want to preserve more of the pack on an error. try { const downloadStickerJob = async ( - stickerProto: Proto.StickerPack.ISticker + stickerProto: Proto.StickerPack.Sticker.Params ): Promise => { try { const stickerInfo = await downloadSticker( diff --git a/ts/util/BodyRange.node.ts b/ts/util/BodyRange.node.ts index 7db8deb662..8701699b10 100644 --- a/ts/util/BodyRange.node.ts +++ b/ts/util/BodyRange.node.ts @@ -3,7 +3,6 @@ import lodash from 'lodash'; -import type { SignalService as Proto } from '../protobuf/index.std.js'; import { BodyRange, type RawBodyRange, @@ -26,7 +25,7 @@ const MAX_PER_TYPE = 250; // We drop unknown bodyRanges and remove extra stuff so they serialize properly export function filterAndClean( - ranges: ReadonlyArray | undefined | null + ranges: ReadonlyArray | undefined | null ): ReadonlyArray | undefined { if (!ranges) { return undefined; @@ -108,7 +107,7 @@ export function filterAndClean( } export function hydrateRanges( - ranges: ReadonlyArray> | undefined, + ranges: ReadonlyArray | undefined, conversationSelector: (id: string) => { id: string; title: string } ): Array | undefined { if (!ranges) { diff --git a/ts/util/ServiceId.node.ts b/ts/util/ServiceId.node.ts index d35c407c89..aaab7cad0d 100644 --- a/ts/util/ServiceId.node.ts +++ b/ts/util/ServiceId.node.ts @@ -78,6 +78,12 @@ export function fromAciUuidBytesOrString( context: string ): AciString; +export function fromAciUuidBytesOrString( + bytes: Uint8Array | undefined | null, + fallback: string, + context: string +): AciString; + export function fromAciUuidBytesOrString( bytes: Uint8Array | undefined | null, fallback: string | undefined | null, diff --git a/ts/util/arePinnedConversationsEqual.node.ts b/ts/util/arePinnedConversationsEqual.node.ts index 6286f99b77..a3bacbb302 100644 --- a/ts/util/arePinnedConversationsEqual.node.ts +++ b/ts/util/arePinnedConversationsEqual.node.ts @@ -6,7 +6,7 @@ import * as Bytes from '../Bytes.std.js'; import { SignalService as Proto } from '../protobuf/index.std.js'; import { fromServiceIdBinaryOrString } from './ServiceId.node.js'; -import PinnedConversation = Proto.AccountRecord.IPinnedConversation; +import PinnedConversation = Proto.AccountRecord.PinnedConversation.Params; export function arePinnedConversationsEqual( localValue: Array, @@ -17,10 +17,17 @@ export function arePinnedConversationsEqual( } return localValue.every( (localPinnedConversation: PinnedConversation, index: number) => { - const remotePinnedConversation = remoteValue[index]; + const remotePinnedConversation = remoteValue[index].identifier; + if (!remotePinnedConversation) { + return false; + } + + if (!localPinnedConversation.identifier) { + return false; + } const { contact, groupMasterKey, legacyGroupId } = - localPinnedConversation; + localPinnedConversation.identifier; if (contact) { const { contact: remoteContact } = remotePinnedConversation; diff --git a/ts/util/attachments.preload.ts b/ts/util/attachments.preload.ts index fb2afba01e..94f87525d8 100644 --- a/ts/util/attachments.preload.ts +++ b/ts/util/attachments.preload.ts @@ -15,6 +15,8 @@ import { canBeTranscoded } from './Attachment.std.js'; import * as Errors from '../types/errors.std.js'; import * as Bytes from '../Bytes.std.js'; +import { toNumber } from './toNumber.std.js'; + const log = createLogger('attachments'); // All outgoing images go through handleImageAttachment before being sent and thus have @@ -100,8 +102,8 @@ export function copyCdnFields( return {}; } return { - cdnId: dropNull(uploaded.cdnId)?.toString(), - cdnKey: uploaded.cdnKey, + cdnId: undefined, + cdnKey: uploaded.attachmentIdentifier.cdnKey, cdnNumber: dropNull(uploaded.cdnNumber), digest: Bytes.toBase64(uploaded.digest), incrementalMac: uploaded.incrementalMac @@ -110,6 +112,6 @@ export function copyCdnFields( chunkSize: dropNull(uploaded.chunkSize), key: Bytes.toBase64(uploaded.key), plaintextHash: uploaded.plaintextHash, - uploadTimestamp: uploaded.uploadTimestamp?.toNumber(), + uploadTimestamp: toNumber(uploaded.uploadTimestamp) ?? undefined, }; } diff --git a/ts/util/backupSubscriptionData.preload.ts b/ts/util/backupSubscriptionData.preload.ts index 464b3685ab..4bce3e7135 100644 --- a/ts/util/backupSubscriptionData.preload.ts +++ b/ts/util/backupSubscriptionData.preload.ts @@ -1,7 +1,6 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import Long from 'long'; import type { Backups, SignalService } from '../protobuf/index.std.js'; import * as Bytes from '../Bytes.std.js'; import { backupsService } from '../services/backups/index.preload.js'; @@ -17,8 +16,8 @@ const log = createLogger('BackupSubscriptionData'); // we'll need separate logic for each export async function saveBackupsSubscriberData( backupsSubscriberData: - | Backups.AccountData.IIAPSubscriberData - | SignalService.AccountRecord.IIAPSubscriberData + | Backups.AccountData.IAPSubscriberData.Params + | SignalService.AccountRecord.IAPSubscriberData.Params | null | undefined ): Promise { @@ -35,8 +34,7 @@ export async function saveBackupsSubscriberData( return; } - const { subscriberId, purchaseToken, originalTransactionId } = - backupsSubscriberData; + const { subscriberId, iapSubscriptionId } = backupsSubscriberData; if (Bytes.isNotEmpty(subscriberId)) { await itemStorage.put('backupsSubscriberId', subscriberId); @@ -44,16 +42,19 @@ export async function saveBackupsSubscriberData( await itemStorage.remove('backupsSubscriberId'); } - if (purchaseToken) { - await itemStorage.put('backupsSubscriberPurchaseToken', purchaseToken); + if (iapSubscriptionId?.purchaseToken != null) { + await itemStorage.put( + 'backupsSubscriberPurchaseToken', + iapSubscriptionId.purchaseToken + ); } else { await itemStorage.remove('backupsSubscriberPurchaseToken'); } - if (originalTransactionId) { + if (iapSubscriptionId?.originalTransactionId != null) { await itemStorage.put( 'backupsSubscriberOriginalTransactionId', - originalTransactionId.toString() + iapSubscriptionId.originalTransactionId.toString() ); } else { await itemStorage.remove('backupsSubscriberOriginalTransactionId'); @@ -72,27 +73,38 @@ export async function saveBackupTier( } } -export function generateBackupsSubscriberData(): Backups.AccountData.IIAPSubscriberData | null { - const backupsSubscriberId = itemStorage.get('backupsSubscriberId'); +export function generateBackupsSubscriberData(): Backups.AccountData.IAPSubscriberData.Params | null { + const subscriberId = itemStorage.get('backupsSubscriberId') ?? null; - if (Bytes.isEmpty(backupsSubscriberId)) { + if (Bytes.isEmpty(subscriberId)) { return null; } - const backupsSubscriberData: Backups.AccountData.IIAPSubscriberData = { - subscriberId: backupsSubscriberId, - }; + let backupsSubscriberData: Backups.AccountData.IAPSubscriberData.Params; const purchaseToken = itemStorage.get('backupsSubscriberPurchaseToken'); if (purchaseToken) { - backupsSubscriberData.purchaseToken = purchaseToken; + backupsSubscriberData = { + subscriberId, + iapSubscriptionId: { + purchaseToken, + }, + }; } else { const originalTransactionId = itemStorage.get( 'backupsSubscriberOriginalTransactionId' ); - if (originalTransactionId) { - backupsSubscriberData.originalTransactionId = Long.fromString( - originalTransactionId - ); + if (originalTransactionId != null) { + backupsSubscriberData = { + subscriberId, + iapSubscriptionId: { + originalTransactionId: BigInt(originalTransactionId), + }, + }; + } else { + backupsSubscriberData = { + subscriberId, + iapSubscriptionId: null, + }; } } diff --git a/ts/util/callDisposition.preload.ts b/ts/util/callDisposition.preload.ts index ff74b7d8fd..7f2a4c4d23 100644 --- a/ts/util/callDisposition.preload.ts +++ b/ts/util/callDisposition.preload.ts @@ -1,7 +1,6 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import Long from 'long'; import type { Call, PeekInfo, LocalDeviceState } from '@signalapp/ringrtc'; import { CallState, @@ -67,7 +66,7 @@ import { drop } from './drop.std.js'; import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync.preload.js'; import { storageServiceUploadJob } from '../services/storage.preload.js'; import { CallLinkFinalizeDeleteManager } from '../jobs/CallLinkFinalizeDeleteManager.preload.js'; -import { parsePartial, parseStrict } from './schemas.std.js'; +import { parseLoose, parseStrict } from './schemas.std.js'; import { calling } from '../services/calling.preload.js'; import { cleanupMessages } from './cleanup.preload.js'; import { MessageModel } from '../models/messages.preload.js'; @@ -141,11 +140,11 @@ export function formatLocalDeviceState( } export function getCallIdFromRing(ringId: bigint): string { - return Long.fromValue(callIdFromRingId(ringId)).toString(); + return BigInt(callIdFromRingId(ringId)).toString(); } export function getCallIdFromEra(eraId: string): string { - return Long.fromValue(callIdFromEra(eraId)).toString(); + return BigInt(callIdFromEra(eraId)).toString(); } export function getCreatorAci(creator: Uint8Array): AciString { @@ -205,11 +204,11 @@ export function convertJoinState(joinState: JoinState): GroupCallJoinState { // ---------------------- export function getCallEventForProto( - callEventProto: Proto.SyncMessage.ICallEvent, + callEventProto: Proto.SyncMessage.CallEvent.Params, eventSource: string ): CallEventDetails { - const callEvent = parsePartial(callEventNormalizeSchema, callEventProto); - const { callId, peerId, timestamp } = callEvent; + const callEvent = parseLoose(callEventNormalizeSchema, callEventProto); + const { callId, conversationId: peerId, timestamp } = callEvent; let type: CallType; if (callEvent.type === Proto.SyncMessage.CallEvent.Type.GROUP_CALL) { @@ -234,10 +233,12 @@ export function getCallEventForProto( } let direction: CallDirection; - if (callEvent.direction === Proto.SyncMessage.CallEvent.Direction.INCOMING) { + if ( + callEventProto.direction === Proto.SyncMessage.CallEvent.Direction.INCOMING + ) { direction = CallDirection.Incoming; } else if ( - callEvent.direction === Proto.SyncMessage.CallEvent.Direction.OUTGOING + callEventProto.direction === Proto.SyncMessage.CallEvent.Direction.OUTGOING ) { direction = CallDirection.Outgoing; } else { @@ -285,12 +286,13 @@ const callLogEventFromProto: Partial< }; export function getCallLogEventForProto( - callLogEventProto: Proto.SyncMessage.ICallLogEvent + callLogEventProto: Proto.SyncMessage.CallLogEvent.Params ): CallLogEventDetails { // CallLogEvent peerId is ambiguous whether it's a conversationId (direct, or groupId) // or roomId so handle both cases - const { peerId: peerIdBytes } = callLogEventProto; - const callLogEvent = parsePartial(callLogEventNormalizeSchema, { + const { conversationId: peerIdBytes } = callLogEventProto; + + const callLogEvent = parseLoose(callLogEventNormalizeSchema, { ...callLogEventProto, peerIdAsConversationId: peerIdBytes, peerIdAsRoomId: peerIdBytes, @@ -365,24 +367,24 @@ export function getBytesForPeerId(callHistory: CallHistoryDetails): Uint8Array { export function getCallIdForProto( callHistory: CallHistoryDetails -): Long | undefined { +): bigint | null { try { - return Long.fromString(callHistory.callId); - } catch (error) { + return BigInt(callHistory.callId); + } catch { // When CallHistory is a placeholder record for call links, then the history item's // callId is invalid. We will ignore it and only send the timestamp. if (callHistory.mode === CallMode.Adhoc) { - return undefined; + return null; } // For other calls, we expect a valid callId. - throw error; + throw new Error(`Invalid callId: ${callHistory.callId}`); } } export function getProtoForCallHistory( callHistory: CallHistoryDetails -): Proto.SyncMessage.ICallEvent | null { +): Proto.SyncMessage.CallEvent.Params { const event = statusToProto[callHistory.status]; strictAssert( @@ -392,14 +394,14 @@ export function getProtoForCallHistory( )}` ); - return new Proto.SyncMessage.CallEvent({ - peerId: getBytesForPeerId(callHistory), + return { + conversationId: getBytesForPeerId(callHistory), callId: getCallIdForProto(callHistory), type: typeToProto[callHistory.type], direction: directionToProto[callHistory.direction], event, - timestamp: Long.fromNumber(callHistory.timestamp), - }); + timestamp: BigInt(callHistory.timestamp), + }; } // Local Events @@ -536,7 +538,7 @@ export function getCallDetailsFromDirectCall( ): CallDetails { const ringerId = call.isIncoming ? call.remoteUserId : null; return parseStrict(callDetailsSchema, { - callId: Long.fromValue(call.callId).toString(), + callId: (call.callId satisfies bigint).toString(), peerId, ringerId, startedById: ringerId, @@ -1304,19 +1306,24 @@ async function updateRemoteCallHistory( try { const ourAci = itemStorage.user.getCheckedAci(); - const syncMessage = MessageSender.createSyncMessage(); - - syncMessage.callEvent = getProtoForCallHistory(callHistory); - - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; + const syncMessage = MessageSender.padSyncMessage({ + content: { + callEvent: getProtoForCallHistory(callHistory), + }, + }); await singleProtoJobQueue.add({ contentHint: ContentHint.Resendable, serviceId: ourAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + senderKeyDistributionMessage: null, + pniSignatureMessage: null, + }) ), type: 'callEventSync', urgent: false, @@ -1457,28 +1464,34 @@ export async function markAllCallHistoryReadAndSync( const ourAci = itemStorage.user.getCheckedAci(); - const callLogEvent = new Proto.SyncMessage.CallLogEvent({ + const callLogEvent: Proto.SyncMessage.CallLogEvent.Params = { type: inConversation ? Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ_IN_CONVERSATION : Proto.SyncMessage.CallLogEvent.Type.MARKED_AS_READ, - timestamp: Long.fromNumber(latestCall.timestamp), - peerId: getBytesForPeerId(latestCall), + timestamp: BigInt(latestCall.timestamp), + conversationId: getBytesForPeerId(latestCall), callId: getCallIdForProto(latestCall), + }; + + const syncMessage = MessageSender.padSyncMessage({ + content: { + callLogEvent, + }, }); - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.callLogEvent = callLogEvent; - - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; - log.info('markAllCallHistoryReadAndSync: Queueing sync message'); await singleProtoJobQueue.add({ contentHint: ContentHint.Resendable, serviceId: ourAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + senderKeyDistributionMessage: null, + pniSignatureMessage: null, + }) ), type: 'callLogEventSync', urgent: false, diff --git a/ts/util/callingMessageToProto.node.ts b/ts/util/callingMessageToProto.node.ts index 9a53b45749..280a2a08da 100644 --- a/ts/util/callingMessageToProto.node.ts +++ b/ts/util/callingMessageToProto.node.ts @@ -3,7 +3,6 @@ import type { CallingMessage } from '@signalapp/ringrtc'; import { CallMessageUrgency } from '@signalapp/ringrtc'; -import Long from 'long'; import { SignalService as Proto } from '../protobuf/index.std.js'; import { createLogger } from '../logging/log.std.js'; import { toLogFormat } from '../types/errors.std.js'; @@ -22,17 +21,18 @@ export function callingMessageToProto( opaque, }: CallingMessage, urgency?: CallMessageUrgency -): Proto.ICallMessage { - let opaqueField: undefined | Proto.CallMessage.IOpaque; +): Proto.CallMessage.Params { + let opaqueField: undefined | Proto.CallMessage.Opaque.Params; if (opaque) { opaqueField = { ...opaque, - data: opaque.data, + urgency: null, + data: opaque.data ?? null, }; } if (urgency !== undefined) { opaqueField = { - ...(opaqueField ?? {}), + ...(opaqueField ?? { data: null, urgency: null }), urgency: urgencyToProto(urgency), }; } @@ -41,42 +41,42 @@ export function callingMessageToProto( offer: offer ? { ...offer, - id: Long.fromValue(offer.callId), + id: offer.callId, type: offer.type as number, opaque: offer.opaque, } - : undefined, + : null, answer: answer ? { ...answer, - id: Long.fromValue(answer.callId), + id: answer.callId, opaque: answer.opaque, } - : undefined, + : null, iceUpdate: iceCandidates - ? iceCandidates.map((candidate): Proto.CallMessage.IIceUpdate => { + ? iceCandidates.map((candidate): Proto.CallMessage.IceUpdate.Params => { return { ...candidate, - id: Long.fromValue(candidate.callId), + id: candidate.callId, opaque: candidate.opaque, }; }) - : undefined, + : null, busy: busy ? { ...busy, - id: Long.fromValue(busy.callId), + id: busy.callId, } - : undefined, + : null, hangup: hangup ? { ...hangup, - id: Long.fromValue(hangup.callId), + id: hangup.callId, type: hangup.type as number, } - : undefined, - destinationDeviceId, - opaque: opaqueField, + : null, + destinationDeviceId: destinationDeviceId ?? null, + opaque: opaqueField ?? null, }; } diff --git a/ts/util/checkFirstEnvelope.dom.ts b/ts/util/checkFirstEnvelope.dom.ts index 92c03f5c1f..715817c661 100644 --- a/ts/util/checkFirstEnvelope.dom.ts +++ b/ts/util/checkFirstEnvelope.dom.ts @@ -12,6 +12,8 @@ import { isProduction } from './version.std.js'; import type { IncomingWebSocketRequest } from '../textsecure/WebsocketResources.preload.js'; +import { toNumber } from './toNumber.std.js'; + const { isNumber } = lodash; const log = createLogger('checkFirstEnvelope'); @@ -33,7 +35,7 @@ export function checkFirstEnvelope(incoming: IncomingWebSocketRequest): void { } const decoded = Proto.Envelope.decode(plaintext); - const newEnvelopeTimestamp = decoded.clientTimestamp?.toNumber(); + const newEnvelopeTimestamp = toNumber(decoded.clientTimestamp); if (!isNumber(newEnvelopeTimestamp)) { log.warn('timestamp is not a number!'); return; diff --git a/ts/util/computeBlurHashUrl.std.ts b/ts/util/computeBlurHashUrl.std.ts index 403936f824..3dec5daa96 100644 --- a/ts/util/computeBlurHashUrl.std.ts +++ b/ts/util/computeBlurHashUrl.std.ts @@ -65,7 +65,8 @@ export function computeBlurHashUrl( desiredWidth = 1, desiredHeight = 1 ): string { - const invAspect = Math.abs(desiredHeight) / (Math.abs(desiredWidth) + 1e-23); + const invAspect = + (Math.abs(desiredHeight) + 1e-23) / (Math.abs(desiredWidth) + 1e-23); // Calculate width and height that roughly satisfy the desired PIXEL_COUNT // diff --git a/ts/util/deleteStoryForEveryone.preload.ts b/ts/util/deleteStoryForEveryone.preload.ts index 3bce894224..96c3047da5 100644 --- a/ts/util/deleteStoryForEveryone.preload.ts +++ b/ts/util/deleteStoryForEveryone.preload.ts @@ -207,7 +207,12 @@ export async function deleteStoryForEveryone( { destinationServiceId, timestamp: story.timestamp, - storyMessageRecipients: newStoryMessageRecipients, + storyMessageRecipients: newStoryMessageRecipients.map(recipient => { + return { + ...recipient, + destinationServiceId: recipient.destinationServiceId ?? undefined, + }; + }), }, noop ); diff --git a/ts/util/denyPendingApprovalRequest.preload.ts b/ts/util/denyPendingApprovalRequest.preload.ts index 1845df364a..3fba122372 100644 --- a/ts/util/denyPendingApprovalRequest.preload.ts +++ b/ts/util/denyPendingApprovalRequest.preload.ts @@ -15,7 +15,7 @@ const log = createLogger('denyPendingApprovalRequest'); export async function denyPendingApprovalRequest( conversationAttributes: ConversationAttributesType, aci: AciString -): Promise { +): Promise { const idLog = getConversationIdForLogging(conversationAttributes); // This user's pending state may have changed in the time between the user's diff --git a/ts/util/encodeDelimited.std.ts b/ts/util/encodeDelimited.std.ts new file mode 100644 index 0000000000..7708d25d85 --- /dev/null +++ b/ts/util/encodeDelimited.std.ts @@ -0,0 +1,36 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export function encodeDelimited(buf: Uint8Array): [Uint8Array, Uint8Array] { + const len = buf.byteLength; + let prefix: Uint8Array; + /* eslint-disable no-bitwise */ + if (len < 0x80) { + prefix = new Uint8Array(1); + prefix[0] = len; + } else if (len < 0x4000) { + prefix = new Uint8Array(2); + prefix[0] = 0x80 | (len & 0x7f); + prefix[1] = len >>> 7; + } else if (len < 0x200000) { + prefix = new Uint8Array(3); + prefix[0] = 0x80 | (len & 0x7f); + prefix[1] = 0x80 | ((len >>> 7) & 0x7f); + prefix[2] = len >>> 14; + } else if (len < 0x10000000) { + prefix = new Uint8Array(4); + prefix[0] = 0x80 | (len & 0x7f); + prefix[1] = 0x80 | ((len >>> 7) & 0x7f); + prefix[2] = 0x80 | ((len >>> 14) & 0x7f); + prefix[3] = len >>> 21; + } else { + prefix = new Uint8Array(5); + prefix[0] = 0x80 | (len & 0x7f); + prefix[1] = 0x80 | ((len >>> 7) & 0x7f); + prefix[2] = 0x80 | ((len >>> 14) & 0x7f); + prefix[3] = 0x80 | ((len >>> 21) & 0x7f); + prefix[4] = len >>> 28; + } + /* eslint-enable no-bitwise */ + return [prefix, buf]; +} diff --git a/ts/util/handleRetry.preload.ts b/ts/util/handleRetry.preload.ts index 644abd3f4f..bc1b07b341 100644 --- a/ts/util/handleRetry.preload.ts +++ b/ts/util/handleRetry.preload.ts @@ -224,13 +224,13 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise { timestamp, }); // eslint-disable-next-line prefer-destructuring - let contentProto: Proto.IContent | undefined = + let contentProto: Proto.Content.Params | undefined = addSenderKeyResult.contentProto; const { groupId } = addSenderKeyResult; // Assert that the requesting UUID is still part of a story distribution list that // the message was sent to, and add its sender key distribution message (SKDM). - if (contentProto.storyMessage && !groupId) { + if (contentProto.content?.storyMessage && !groupId) { contentProto = await checkDistributionListAndAddSKDM({ confirm, contentProto, @@ -243,20 +243,19 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise { return; } } - const story = Boolean(contentProto.storyMessage); + const story = Boolean(contentProto.content?.storyMessage); const recipientConversation = window.ConversationController.getOrCreate( requesterAci, 'private' ); - const protoToSend = new Proto.Content(contentProto); await conversationJobQueue.add({ type: 'SavedProto', conversationId: recipientConversation.id, contentHint, groupId, - protoBase64: Bytes.toBase64(Proto.Content.encode(protoToSend).finish()), + protoBase64: Bytes.toBase64(Proto.Content.encode(contentProto)), story, timestamp, urgent, @@ -481,13 +480,13 @@ async function checkDistributionListAndAddSKDM({ requesterAci, messaging, }: { - contentProto: Proto.IContent; + contentProto: Proto.Content.Params; timestamp: number; confirm: () => void; requesterAci: AciString; logId: string; messaging: MessageSender; -}): Promise { +}): Promise { let distributionList: StoryDistributionListDataType | undefined; const { storyDistributionLists } = window.reduxStore.getState(); const membersByListId = new Map>(); @@ -562,14 +561,14 @@ async function maybeAddSenderKeyDistributionMessage({ requesterAci, timestamp, }: { - contentProto: Proto.IContent; + contentProto: Proto.Content.Params; logId: string; messageIds: Array; requestGroupId?: string; requesterAci: AciString; timestamp: number; }): Promise<{ - contentProto: Proto.IContent; + contentProto: Proto.Content.Params; groupId?: string; }> { const conversation = await getRetryConversation({ diff --git a/ts/util/inspectProtobufs.std.ts b/ts/util/inspectProtobufs.std.ts index 3de91194c3..dfe66bc31c 100644 --- a/ts/util/inspectProtobufs.std.ts +++ b/ts/util/inspectProtobufs.std.ts @@ -6,7 +6,7 @@ import protobufjs from 'protobufjs'; const { Reader } = protobufjs; type MessageWithUnknownFields = { - $unknownFields?: ReadonlyArray; + $unknown?: ReadonlyArray; }; /** @@ -38,7 +38,7 @@ export function inspectUnknownFieldTags( message: MessageWithUnknownFields ): Array { return ( - message.$unknownFields?.map(field => { + message.$unknown?.map(field => { // https://protobuf.dev/programming-guides/encoding/ // The first byte of a field is a varint encoding the tag bit-shifted << 3 // eslint-disable-next-line no-bitwise diff --git a/ts/util/isKnownProtoEnumMember.std.ts b/ts/util/isKnownProtoEnumMember.std.ts new file mode 100644 index 0000000000..5c6c953c2d --- /dev/null +++ b/ts/util/isKnownProtoEnumMember.std.ts @@ -0,0 +1,9 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export function isKnownProtoEnumMember( + enum_: Record, + value: unknown +): value is E { + return typeof value === 'number' && Object.hasOwn(enum_, value); +} diff --git a/ts/util/long.std.ts b/ts/util/long.std.ts new file mode 100644 index 0000000000..199918cb54 --- /dev/null +++ b/ts/util/long.std.ts @@ -0,0 +1,4 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export const MAX_VALUE = 0xffffffff_ffffffffn; diff --git a/ts/util/onStoryRecipientUpdate.preload.ts b/ts/util/onStoryRecipientUpdate.preload.ts index 3f7b7ac3de..d8bce6e02d 100644 --- a/ts/util/onStoryRecipientUpdate.preload.ts +++ b/ts/util/onStoryRecipientUpdate.preload.ts @@ -12,7 +12,6 @@ import { isStory } from '../state/selectors/message.preload.js'; import { queueUpdateMessage } from './messageBatcher.preload.js'; import { isMe } from './whatTypeOfConversation.dom.js'; import { drop } from './drop.std.js'; -import { fromServiceIdBinaryOrString } from './ServiceId.node.js'; import { applyDeleteForEveryone } from './deleteForEveryone.preload.js'; import { itemStorage } from '../textsecure/Storage.preload.js'; import { MessageModel } from '../models/messages.preload.js'; @@ -64,16 +63,7 @@ export async function onStoryRecipientUpdate( Set >(); data.storyMessageRecipients.forEach(item => { - const { - destinationServiceId: rawDestinationServiceId, - destinationServiceIdBinary, - } = item; - - const recipientServiceId = fromServiceIdBinaryOrString( - destinationServiceIdBinary, - rawDestinationServiceId, - `${logId}.recipientServiceId` - ); + const { destinationServiceId: recipientServiceId } = item; if (recipientServiceId == null) { return; diff --git a/ts/util/removePendingMember.preload.ts b/ts/util/removePendingMember.preload.ts index 75487399b7..2b56ca1cf4 100644 --- a/ts/util/removePendingMember.preload.ts +++ b/ts/util/removePendingMember.preload.ts @@ -15,7 +15,7 @@ const log = createLogger('removePendingMember'); export async function removePendingMember( conversationAttributes: ConversationAttributesType, serviceIds: ReadonlyArray -): Promise { +): Promise { const idLog = getConversationIdForLogging(conversationAttributes); const pendingServiceIds = serviceIds diff --git a/ts/util/sendCallLinkUpdateSync.preload.ts b/ts/util/sendCallLinkUpdateSync.preload.ts index c50c15541f..214c4676d7 100644 --- a/ts/util/sendCallLinkUpdateSync.preload.ts +++ b/ts/util/sendCallLinkUpdateSync.preload.ts @@ -43,26 +43,32 @@ async function _sendCallLinkUpdateSync( try { const ourAci = itemStorage.user.getCheckedAci(); - const callLinkUpdate = new Proto.SyncMessage.CallLinkUpdate({ + const callLinkUpdate: Proto.SyncMessage.CallLinkUpdate.Params = { type: protoType, rootKey: toRootKeyBytes(callLink.rootKey), adminPasskey: callLink.adminKey ? toAdminKeyBytes(callLink.adminKey) : null, + }; + + const syncMessage = MessageSender.padSyncMessage({ + content: { + callLinkUpdate, + }, }); - const syncMessage = MessageSender.createSyncMessage(); - syncMessage.callLinkUpdate = callLinkUpdate; - - const contentMessage = new Proto.Content(); - contentMessage.syncMessage = syncMessage; - await singleProtoJobQueue.add({ contentHint: ContentHint.Resendable, serviceId: ourAci, isSyncMessage: true, protoBase64: Bytes.toBase64( - Proto.Content.encode(contentMessage).finish() + Proto.Content.encode({ + content: { + syncMessage, + }, + senderKeyDistributionMessage: null, + pniSignatureMessage: null, + }) ), type: 'callLinkUpdateSync', urgent: false, diff --git a/ts/util/sendToGroup.preload.ts b/ts/util/sendToGroup.preload.ts index 02e3041f34..91b067de57 100644 --- a/ts/util/sendToGroup.preload.ts +++ b/ts/util/sendToGroup.preload.ts @@ -183,7 +183,7 @@ export async function sendToGroup({ type SendToGroupOptions = Readonly<{ contentHint: number; - contentMessage: Proto.Content; + contentMessage: Proto.Content.Params; isPartialSend?: boolean; messageId: string | undefined; online?: boolean; @@ -249,7 +249,7 @@ export async function sendContentMessageToGroup( const sendLogCallback = messageSender.makeSendLogCallback({ contentHint, messageId, - proto: Proto.Content.encode(contentMessage).finish(), + proto: Proto.Content.encode(contentMessage), sendType, timestamp, urgent, @@ -563,7 +563,7 @@ export async function sendToGroupViaSenderKey( contentHint, devices: devicesForSenderKey, distributionId, - contentMessage: Proto.Content.encode(contentMessage).finish(), + contentMessage: Proto.Content.encode(contentMessage), groupId, }); @@ -617,7 +617,7 @@ export async function sendToGroupViaSenderKey( sendLogId = await DataWriter.insertSentProto( { contentHint, - proto: Proto.Content.encode(contentMessage).finish(), + proto: Proto.Content.encode(contentMessage), timestamp, urgent, hasPniSignatureMessage: false, @@ -810,18 +810,18 @@ export async function sendToGroupViaSenderKey( // 11. Return early if there are no normal send recipients if (normalSendRecipients.length === 0) { return { - dataMessage: contentMessage.dataMessage - ? Proto.DataMessage.encode(contentMessage.dataMessage).finish() + dataMessage: contentMessage.content?.dataMessage + ? Proto.DataMessage.encode(contentMessage.content.dataMessage) : undefined, - editMessage: contentMessage.editMessage - ? Proto.EditMessage.encode(contentMessage.editMessage).finish() + editMessage: contentMessage.content?.editMessage + ? Proto.EditMessage.encode(contentMessage.content.editMessage) : undefined, successfulServiceIds: senderKeyRecipients, unidentifiedDeliveries: senderKeyRecipients, contentHint, timestamp, - contentProto: Proto.Content.encode(contentMessage).finish(), + contentProto: Proto.Content.encode(contentMessage), recipients: senderKeyRecipientsWithDevices, urgent, }; diff --git a/ts/util/sessionTranslation.node.ts b/ts/util/sessionTranslation.node.ts index 3b001c8b9f..b39d678e4d 100644 --- a/ts/util/sessionTranslation.node.ts +++ b/ts/util/sessionTranslation.node.ts @@ -9,9 +9,6 @@ import { deriveSecrets } from '../Crypto.node.js'; const { get, isFinite, isInteger, isString } = lodash; -const { RecordStructure, SessionStructure } = signal.proto.storage; -const { Chain } = SessionStructure; - type KeyPairType = { privKey?: string; pubKey?: string; @@ -78,19 +75,15 @@ export type LocalUserDataType = { }; export function sessionStructureToBytes( - recordStructure: signal.proto.storage.RecordStructure + recordStructure: signal.proto.storage.RecordStructure.Params ): Uint8Array { - return signal.proto.storage.RecordStructure.encode(recordStructure).finish(); + return signal.proto.storage.RecordStructure.encode(recordStructure); } export function sessionRecordToProtobuf( record: SessionRecordType, ourData: LocalUserDataType -): signal.proto.storage.RecordStructure { - const proto = new RecordStructure(); - - proto.previousSessions = []; - +): signal.proto.storage.RecordStructure.Params { const sessionGroup = record.sessions || {}; const sessions = Object.values(sessionGroup); @@ -98,8 +91,11 @@ export function sessionRecordToProtobuf( return session?.indexInfo?.closed === -1; }); + let currentSession: signal.proto.storage.SessionStructure.Params | null; if (first) { - proto.currentSession = toProtobufSession(first, ourData); + currentSession = toProtobufSession(first, ourData); + } else { + currentSession = null; } sessions.sort((left, right) => { @@ -114,69 +110,26 @@ export function sessionRecordToProtobuf( throw new Error('toProtobuf: More than one open session!'); } - proto.previousSessions = []; + const previousSessions = + new Array(); onlyClosed.forEach(session => { - proto.previousSessions.push(toProtobufSession(session, ourData)); + previousSessions.push(toProtobufSession(session, ourData)); }); - if (!proto.currentSession && proto.previousSessions.length === 0) { + if (!currentSession && previousSessions.length === 0) { throw new Error('toProtobuf: Record had no sessions!'); } - return proto; + return { + currentSession, + previousSessions, + }; } function toProtobufSession( session: SessionType, ourData: LocalUserDataType -): signal.proto.storage.SessionStructure { - const proto = new SessionStructure(); - - // Core Fields - - proto.aliceBaseKey = binaryToUint8Array(session, 'indexInfo.baseKey', 33); - proto.localIdentityPublic = ourData.identityKeyPublic; - proto.localRegistrationId = ourData.registrationId; - - proto.previousCounter = - getInteger(session, 'currentRatchet.previousCounter') + 1; - proto.remoteIdentityPublic = binaryToUint8Array( - session, - 'indexInfo.remoteIdentityKey', - 33 - ); - proto.remoteRegistrationId = getInteger(session, 'registrationId'); - proto.rootKey = binaryToUint8Array(session, 'currentRatchet.rootKey', 32); - proto.sessionVersion = 3; - - // Note: currently unused - // proto.needsRefresh = null; - - // Pending PreKey - - if (session.pendingPreKey) { - proto.pendingPreKey = - new signal.proto.storage.SessionStructure.PendingPreKey(); - proto.pendingPreKey.baseKey = binaryToUint8Array( - session, - 'pendingPreKey.baseKey', - 33 - ); - proto.pendingPreKey.signedPreKeyId = getInteger( - session, - 'pendingPreKey.signedKeyId' - ); - - if (session.pendingPreKey.preKeyId !== undefined) { - proto.pendingPreKey.preKeyId = getInteger( - session, - 'pendingPreKey.preKeyId' - ); - } - } - - // Sender Chain - +): signal.proto.storage.SessionStructure.Params { const senderBaseKey = session.currentRatchet?.ephemeralKeyPair?.pubKey; if (!senderBaseKey) { throw new Error('toProtobufSession: No sender base key!'); @@ -208,12 +161,8 @@ function toProtobufSession( 32 ); - proto.senderChain = protoSenderChain; - // First Receiver Chain - proto.receiverChains = []; - const firstReceiverChainBaseKey = session.currentRatchet?.lastRemoteEphemeralKey; if (!firstReceiverChainBaseKey) { @@ -225,6 +174,9 @@ function toProtobufSession( | ChainType | undefined; + const receiverChains = + new Array(); + // If the session was just initialized, then there will be no receiver chain if (firstReceiverChain) { const protoFirstReceiverChain = toProtobufChain(firstReceiverChain); @@ -241,7 +193,7 @@ function toProtobufSession( 33 ); - proto.receiverChains.push(protoFirstReceiverChain); + receiverChains.push(protoFirstReceiverChain); } // Old Receiver Chains @@ -277,40 +229,79 @@ function toProtobufSession( 33 ); - proto.receiverChains.push(protoChain); + receiverChains.push(protoChain); }); - return proto; + return { + // Core Fields + aliceBaseKey: binaryToUint8Array(session, 'indexInfo.baseKey', 33), + localIdentityPublic: ourData.identityKeyPublic, + localRegistrationId: ourData.registrationId, + + previousCounter: getInteger(session, 'currentRatchet.previousCounter') + 1, + remoteIdentityPublic: binaryToUint8Array( + session, + 'indexInfo.remoteIdentityKey', + 33 + ), + remoteRegistrationId: getInteger(session, 'registrationId'), + rootKey: binaryToUint8Array(session, 'currentRatchet.rootKey', 32), + sessionVersion: 3, + + // Note: currently unused + needsRefresh: null, + + // Pending PreKey + pendingPreKey: session.pendingPreKey + ? { + baseKey: binaryToUint8Array(session, 'pendingPreKey.baseKey', 33), + signedPreKeyId: getInteger(session, 'pendingPreKey.signedKeyId'), + preKeyId: + session.pendingPreKey.preKeyId !== undefined + ? getInteger(session, 'pendingPreKey.preKeyId') + : null, + } + : null, + + // Sender Chain + + senderChain: protoSenderChain, + + receiverChains, + }; } function toProtobufChain( chain: ChainType -): signal.proto.storage.SessionStructure.Chain { - const proto = new Chain(); - - const protoChainKey = new Chain.ChainKey(); - protoChainKey.index = getInteger(chain, 'chainKey.counter') + 1; - if (chain.chainKey?.key !== undefined) { - protoChainKey.key = binaryToUint8Array(chain, 'chainKey.key', 32); - } - proto.chainKey = protoChainKey; - +): signal.proto.storage.SessionStructure.Chain.Params { const messageKeys = Object.entries(chain.messageKeys || {}); - proto.messageKeys = messageKeys.map(entry => { - const protoMessageKey = new SessionStructure.Chain.MessageKey(); - protoMessageKey.index = getInteger(entry, '0') + 1; - const key = binaryToUint8Array(entry, '1', 32); - const { cipherKey, macKey, iv } = translateMessageKey(key); + return { + chainKey: { + index: getInteger(chain, 'chainKey.counter') + 1, + key: + chain.chainKey?.key !== undefined + ? binaryToUint8Array(chain, 'chainKey.key', 32) + : null, + }, + messageKeys: messageKeys.map( + ( + entry + ): signal.proto.storage.SessionStructure.Chain.MessageKey.Params => { + const key = binaryToUint8Array(entry, '1', 32); - protoMessageKey.cipherKey = cipherKey; - protoMessageKey.macKey = macKey; - protoMessageKey.iv = iv; - - return protoMessageKey; - }); - - return proto; + const { cipherKey, macKey, iv } = translateMessageKey(key); + return { + index: getInteger(entry, '0') + 1, + cipherKey, + macKey, + iv, + }; + } + ), + senderRatchetKey: null, + senderRatchetKeyPrivate: null, + }; } // Utility functions diff --git a/ts/util/timestampLongUtils.std.ts b/ts/util/timestampLongUtils.std.ts index 6c9a0e34fa..c877e13fec 100644 --- a/ts/util/timestampLongUtils.std.ts +++ b/ts/util/timestampLongUtils.std.ts @@ -1,33 +1,32 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import Long from 'long'; - import { MAX_SAFE_DATE } from './timestamp.std.js'; +import { toNumber } from './toNumber.std.js'; export function getSafeLongFromTimestamp( timestamp = 0, - maxValue: Long | number = MAX_SAFE_DATE -): Long { + maxValue: bigint | number = MAX_SAFE_DATE +): bigint { if (timestamp >= MAX_SAFE_DATE) { if (typeof maxValue === 'number') { - return Long.fromNumber(maxValue); + return BigInt(maxValue); } return maxValue; } - return Long.fromNumber(timestamp); + return BigInt(timestamp); } export function getTimestampFromLong( - value?: Long | null, + value?: bigint | null, maxValue = MAX_SAFE_DATE ): number { - if (!value || value.isNegative()) { + if (!value || value < 0n) { return 0; } - const num = value.toNumber(); + const num = toNumber(value); if (num > MAX_SAFE_DATE) { return maxValue; @@ -42,12 +41,12 @@ export class InvalidTimestampError extends Error { } } -export function getCheckedTimestampFromLong(value?: Long | null): number { +export function getCheckedTimestampFromLong(value?: bigint | null): number { if (value == null) { throw new InvalidTimestampError('No number'); } - const num = value.toNumber(); + const num = toNumber(value); if (num < 0) { throw new InvalidTimestampError('Underflow'); @@ -61,9 +60,9 @@ export function getCheckedTimestampFromLong(value?: Long | null): number { } export function getTimestampOrUndefinedFromLong( - value?: Long | null + value?: bigint | null ): number | undefined { - if (!value || value.isZero()) { + if (!value || value === 0n) { return undefined; } @@ -71,9 +70,9 @@ export function getTimestampOrUndefinedFromLong( } export function getCheckedTimestampOrUndefinedFromLong( - value?: Long | null + value?: bigint | null ): number | undefined { - if (!value || value.isZero()) { + if (!value || value === 0n) { return undefined; } diff --git a/ts/util/uploadAttachment.preload.ts b/ts/util/uploadAttachment.preload.ts index de520ba807..6923af6579 100644 --- a/ts/util/uploadAttachment.preload.ts +++ b/ts/util/uploadAttachment.preload.ts @@ -1,6 +1,5 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import Long from 'long'; import { createReadStream } from 'node:fs'; import type { AttachmentType, @@ -100,24 +99,28 @@ export async function uploadAttachment( const fileName = shouldStripFilename ? undefined : attachment.fileName; return { - cdnKey, + attachmentIdentifier: { + cdnKey, + }, cdnNumber, - clientUuid: clientUuid ? uuidToBytes(clientUuid) : undefined, + clientUuid: clientUuid ? uuidToBytes(clientUuid) : null, key: keys, size: attachment.data.byteLength, digest, plaintextHash, - incrementalMac, - chunkSize, - uploadTimestamp: Long.fromNumber(uploadTimestamp), + incrementalMac: incrementalMac ?? null, + chunkSize: chunkSize ?? null, + uploadTimestamp: BigInt(uploadTimestamp), contentType: MIMETypeToString(attachment.contentType), - fileName, - flags, - width, - height, - caption, - blurHash, + fileName: fileName ?? null, + flags: flags ?? null, + width: width ?? null, + height: height ?? null, + caption: caption ?? null, + blurHash: blurHash ?? null, + + thumbnail: null, }; }