Compare commits

...

83 commits
v0.3.0 ... main

Author SHA1 Message Date
d31dad7179
Merge pull request #422 from twonlyapp/dev
Dev
2026-07-13 23:22:17 +02:00
9a579e818c bump version
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-07-13 23:19:48 +02:00
722466cc34 fix some ui issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-07-13 23:04:49 +02:00
8fc8907a87 fix downloaded media files not shown 2026-07-13 22:34:08 +02:00
3ed6c1b334 remove security profiles 2026-07-13 22:19:31 +02:00
f6f865a6e6 fix strings 2026-07-13 22:10:56 +02:00
93f1031453 fix in chat overview
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-07-09 21:08:05 +02:00
21ca0a9c37 fix missing openToVerify 2026-07-09 20:55:48 +02:00
48be46cf47 pop until the users was verified 2026-07-09 20:43:37 +02:00
dfa6ac2cfe improve media download logging 2026-07-09 20:33:46 +02:00
cda7df36f3 Improve: Visibility of the verification badge
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-07-09 18:09:16 +02:00
cda52a5da8 fix timer issues 2026-07-09 16:35:42 +02:00
2cee00aff6 fix session out of sync performance issues 2026-07-09 16:35:34 +02:00
599882700c uncomment link 2026-07-07 22:54:19 +02:00
d70ad8fb85 bump version
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-07-07 22:44:49 +02:00
43390871c2 filter reuploaded images 2026-07-07 22:44:20 +02:00
511490b649 filter annouced users 2026-07-07 22:36:13 +02:00
07037537e1 fix performance issue 2026-07-07 22:29:10 +02:00
3cdbd69d88 fix aspect ratio
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-07-07 22:06:51 +02:00
1126d4172b fixing phantom push notification 2026-07-07 21:47:58 +02:00
04680e0855 fix smaller issues 2026-07-07 21:41:50 +02:00
cec25675c3 use normal font 2026-07-07 21:41:42 +02:00
7a8901cd2d fix performance issue 2026-07-07 21:26:42 +02:00
9faa4fc49a send the threshold to the other user
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-06-23 23:25:30 +02:00
991376e1d9 poc for recovery works
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-23 21:07:26 +02:00
7ea311236d working server integration
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-23 00:50:46 +02:00
ca77cc4902 store the mac on the server
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-21 22:01:40 +02:00
f8a2240992 implemented server logic 2026-06-21 17:11:01 +02:00
00b2b25252 modify passwordles recovery settings
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-21 03:40:35 +02:00
4d943eca1c enable and handle heartbeath 2026-06-21 02:07:09 +02:00
6db45a6da8 fix loader issue 2026-06-20 15:44:36 +02:00
e000b7c41d require authentication for backup password change
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-20 15:38:06 +02:00
e7dab1102d bump version for internal testing
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-20 13:12:34 +02:00
7066e8c799 fix errors found with glitchtip 2026-06-20 13:11:48 +02:00
e8829ed2c9 improve error logging 2026-06-20 13:09:53 +02:00
6a106a7fa2
Merge pull request #421 from twonlyapp/dev
Dev
2026-06-20 01:51:32 +02:00
4c42d55afd use media id as filename
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-20 01:08:39 +02:00
f14a94d639 fix camera initalization issue 2026-06-20 01:07:20 +02:00
c4fc14a909 remove unused animations and start pre-caching later 2026-06-20 00:01:14 +02:00
19ffed3386 bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-19 18:53:58 +02:00
f278a1dd38 pre cache the chat list view 2026-06-19 17:47:49 +02:00
e4bc21ea10 fix more navigation issues 2026-06-19 17:28:56 +02:00
26c9dc0066 fix notification click did not directly open the chats 2026-06-19 17:04:09 +02:00
0007ba62a0 split up the media viewer into statless widgets 2026-06-19 16:26:12 +02:00
40b645e803 do not hide camera if app life cycle is inactive
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-19 16:00:58 +02:00
4e2b90de33 fix smaller ui issues 2026-06-19 15:57:38 +02:00
9b82310d49 Merge remote-tracking branch 'origin/main' into dev 2026-06-19 00:51:34 +02:00
c7db5b7bdc
Merge pull request #420 from twonlyapp/dev
Dev
2026-06-19 00:51:16 +02:00
690f4cae2f bump version and work forward
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-19 00:45:57 +02:00
c28808537b use render cache for animated icons
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-06-16 22:44:21 +02:00
98fa90a46b start with passwordless recovery
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 20:46:15 +02:00
2687545e33 use svg vector format
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 19:14:14 +02:00
8070f66142 bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 18:00:53 +02:00
be74a94f50 hide buttons if accounced user was set to be hidden 2026-06-16 17:44:04 +02:00
d528913e84 update media view to new mybutton design
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 17:40:57 +02:00
f22e9086ed implement a precacheImage for faster image loading
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 16:59:36 +02:00
e7301020f6 fix: group auto rentry when it was deleted
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 15:04:51 +02:00
e23ac5932e add mising entry for own changes
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 11:44:35 +02:00
96eddd7480 Fix: Shared contacts now correctly show the blue verification badge
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 11:32:39 +02:00
9daf275310 fix svg performance issue
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 10:30:22 +02:00
83fd99ea70 print avatar warning
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 00:57:56 +02:00
0ab6eba47d bump version
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-16 00:22:03 +02:00
d5eae605c3 change order 2026-06-16 00:19:50 +02:00
a7414c6fcc Promotion of sharing contacts when contact is new to twonly 2026-06-16 00:19:40 +02:00
52489a3a98 hide link preview for qr codes in normal camera mode
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-15 23:10:39 +02:00
f8c5ba17c6 remove some info logs 2026-06-15 23:00:24 +02:00
f436137c9b replace push with go to prevent multiple chats open
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-15 22:50:00 +02:00
5dc071a737 open links on external provider 2026-06-15 22:47:45 +02:00
bff567ee3d hide notification for active chats 2026-06-15 22:47:31 +02:00
878aca0f12 fix typing indicator glitching 2026-06-15 22:36:34 +02:00
7dbd190434 improve verification badge faq 2026-06-15 18:38:48 +02:00
2f0e2a9e24 fix performance issues 2026-06-15 18:24:17 +02:00
75079a790c better feedback in case a qr code was shown
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-15 18:04:14 +02:00
0e377d0ebc added chain link animation 2026-06-15 17:33:54 +02:00
7cb1e31e0c
Merge pull request #419 from twonlyapp/dev
- Improved: Design of some UI components
- Improved: Memories viewer shows state for batch operations and has improved performance
- Fix: Issue with background notifications on Android
- Fix: Changed minimum threshold for the user discovery to 3
- Fix: Multiple UI issues
- Fix: Auto-detect if FCM token does not work and trigger a reset
2026-06-10 22:42:01 +02:00
cef58119c5 remove old apks 2026-06-10 22:41:24 +02:00
3981341cd3 bump version
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled
2026-06-10 00:07:18 +02:00
726cdaf89f use text capitalization
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-09 20:59:19 +02:00
23553b9cb0 fix clippy 2026-06-08 22:59:50 +02:00
37551cacce merge into one single crate 2026-06-08 22:58:01 +02:00
053fbeb66e Fix: Issue with background notifications on Android
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-08 22:05:22 +02:00
b7b2ad701f add app lifecycle state for camera controller
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
2026-06-08 10:21:29 +02:00
e2f5f41736 add changelog 2026-06-08 10:14:55 +02:00
262 changed files with 90760 additions and 5915 deletions

View file

@ -1,13 +1,46 @@
# Changelog # Changelog
## 0.2.30 ## 0.3.7
- Fix: Changed minimum threshold for the user discovery to 3 - Fix: Multiple UI issues
## 0.2.28 ## 0.3.6
- Improve: Visibility of the verification badge
## 0.3.5
- Fix: Performance issue caused by an out-of-sync Signal session
- Fix: Multiple smaller bug fixes
## 0.3.3
- Fix: Multiple UI issues
- Fix: Camera initialization issue
## 0.3.2
- Fix: Multiple smaller performance and UI issues
## 0.3.1
- New: Promotion of sharing contacts when contact is new to twonly
- Improve: Onboarding of new users to the verification badges
- Improve: Better feedback when a QR code is scanned
- Fix: Shared contacts now correctly show the blue verification badge
- Fix: Suppressed link previews for scanned QR codes
- Fix: Black screen on iOS when a link is clicked
- Fix: Fixed size of the typing indicator to prevent the chat from glitching
- Fix: Push notifications are not shown for chats that are already open
- Fix: Multiple smaller performance and UI issues
## 0.3.0
- Improved: Design of some UI components - Improved: Design of some UI components
- Improved: Memories viewer shows state for batch operations and has improved performance - Improved: Memories viewer shows state for batch operations and has improved performance
- Fix: Issue with background notifications on Android
- Fix: Changed minimum threshold for the user discovery to 3
- Fix: Multiple UI issues
- Fix: Auto-detect if FCM token does not work and trigger a reset - Fix: Auto-detect if FCM token does not work and trigger a reset
## 0.2.26 ## 0.2.26

View file

@ -35,7 +35,6 @@ If you decide to give twonly a try, please keep in mind that it is still in its
- End-to-End encryption using the [Signal Protocol](https://de.wikipedia.org/wiki/Signal-Protokoll) - End-to-End encryption using the [Signal Protocol](https://de.wikipedia.org/wiki/Signal-Protokoll)
- Open Source and can be downloaded directly from GitHub - Open Source and can be downloaded directly from GitHub
- No email or phone number required to register - No email or phone number required to register
- Privacy friendly - Everything is stored on the device
- The backend is hosted exclusively in Europe - The backend is hosted exclusively in Europe
## Roadmap ## Roadmap
@ -43,7 +42,6 @@ If you decide to give twonly a try, please keep in mind that it is still in its
### Currently ### Currently
- Focus on user-friendliness so that people enjoy using the app - Focus on user-friendliness so that people enjoy using the app
- User discovery without a phone number
- Passwordless recovery without a phone number - Passwordless recovery without a phone number
- Implementation of features so that Snapchat can actually be replaced - Implementation of features so that Snapchat can actually be replaced
- E2EE cloud backup of memories - E2EE cloud backup of memories
@ -63,36 +61,9 @@ If you discover a security issue in twonly, please adhere to the coordinated vul
us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not
guarantee a bounty currently :/ guarantee a bounty currently :/
<!-- ## Contribution ## Contribution
If you have any questions or feature requests, feel free to start a new discussion. Issues are limited to bugs, and for maintainers only. -->
## Development
<details>
<summary>Setup Instructions (macOS)</summary>
## Building
Some dependencies are downloaded directly from the source as there are some new changes which are not yet published on
pub.dev or because they require some special installation.
```bash
git submodule update --init --recursive
cd dependencies/flutter_zxing
git submodule update --init --recursive
./scripts/update_ios_macos_src.s
```
## Debugging files
```bash
run-as eu.twonly.testing ls /data/user/0/eu.twonly.testing/
```
</details>
Currently there are still some core features and rewrites open I want to do like the switch to the MLS protocol involving a huge rewrite in Rust. Because of this, contributions are currently not wanted. You can still view the code (if you find any security issues please contact me!). Also, issues are currently closed. If you find a bug, please use the in-app option as there you can upload your debug log which helps a lot.
## Signing Keys ## Signing Keys

View file

@ -19,6 +19,7 @@ analyzer:
- "lib/core/**" - "lib/core/**"
- "lib/src/localization/**" - "lib/src/localization/**"
- "rust_builder/" - "rust_builder/"
- "scripts/"
- "build/" - "build/"
- "dependencies/**" - "dependencies/**"
- "pubspec.yaml" - "pubspec.yaml"

Binary file not shown.

Binary file not shown.

View file

@ -33,7 +33,7 @@ platform :android do
# Load release notes from CHANGELOG.md # Load release notes from CHANGELOG.md
changelog_path = File.expand_path("../CHANGELOG.md", __dir__) changelog_path = File.expand_path("../CHANGELOG.md", __dir__)
release_notes = "Automated local release via Fastlane" release_notes = "Small bug fixes."
if File.exist?(changelog_path) if File.exist?(changelog_path)
changelog_content = File.read(changelog_path) changelog_content = File.read(changelog_path)
escaped_version = Regexp.escape(version) escaped_version = Regexp.escape(version)
@ -122,6 +122,11 @@ platform :android do
UI.message("Starting F-Droid deployment...") UI.message("Starting F-Droid deployment...")
FileUtils.mkdir_p(fdroid_repo_dir) FileUtils.mkdir_p(fdroid_repo_dir)
# Delete all APK files in the directory
sh("rm -f #{fdroid_repo_dir}/*.apk")
UI.message("All APK files deleted.")
apks.each do |apk_path| apks.each do |apk_path|
basename = File.basename(apk_path) basename = File.basename(apk_path)
new_name = "eu.twonly_v#{version}-#{basename}" new_name = "eu.twonly_v#{version}-#{basename}"

View file

@ -18,33 +18,6 @@
"version" : "11.2.0" "version" : "11.2.0"
} }
}, },
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{ {
"identity" : "firebase-ios-sdk", "identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@ -153,15 +126,6 @@
"version" : "2.4.0" "version" : "2.4.0"
} }
}, },
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{ {
"identity" : "sentry-cocoa", "identity" : "sentry-cocoa",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@ -170,24 +134,6 @@
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae", "revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae",
"version" : "8.58.0" "version" : "8.58.0"
} }
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
} }
], ],
"version" : 2 "version" : 2

View file

@ -18,33 +18,6 @@
"version" : "11.2.0" "version" : "11.2.0"
} }
}, },
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{ {
"identity" : "firebase-ios-sdk", "identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@ -153,15 +126,6 @@
"version" : "2.4.0" "version" : "2.4.0"
} }
}, },
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{ {
"identity" : "sentry-cocoa", "identity" : "sentry-cocoa",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@ -170,24 +134,6 @@
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae", "revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae",
"version" : "8.58.0" "version" : "8.58.0"
} }
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
} }
], ],
"version" : 2 "version" : 2

View file

@ -5,9 +5,13 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/model/json/onboarding_state.model.dart';
import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/visual/components/app_outdated.comp.dart'; import 'package:twonly/src/visual/components/app_outdated.comp.dart';
@ -18,7 +22,7 @@ import 'package:twonly/src/visual/views/home.view.dart';
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/visual/views/onboarding/register.view.dart'; import 'package:twonly/src/visual/views/onboarding/register.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
import 'package:twonly/src/visual/views/recovery.view.dart'; import 'package:twonly/src/visual/views/recovery_from_secure_storage.view.dart';
import 'package:twonly/src/visual/views/unlock_twonly.view.dart'; import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
@ -151,6 +155,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
Future<void> initAsync() async { Future<void> initAsync() async {
Log.info('AppWidgetState: initAsync started'); Log.info('AppWidgetState: initAsync started');
final onboardingState = await KeyValueStore.getModel<OnboardingState>(
KeyValueKeys.onboardingState,
);
_showOnboarding = !onboardingState.hasOnboardingFinished;
if (userService.isUserCreated) { if (userService.isUserCreated) {
if (_initialPage != 0) { if (_initialPage != 0) {
final count = await twonlyDB.contactsDao.getContactsCount(); final count = await twonlyDB.contactsDao.getContactsCount();
@ -180,8 +188,19 @@ class _AppMainWidgetState extends State<AppMainWidget> {
} else { } else {
_proofOfWork = (null, disabled); _proofOfWork = (null, disabled);
} }
if (onboardingState.hasStartedPasswordlessRecovery ||
onboardingState.emailRecoveryRequested) {
WidgetsBinding.instance.addPostFrameCallback((_) {
routerProvider.push(Routes.recoverPasswordless);
});
}
} }
// await PasswordlessRecoveryService.handleRecoveryLink(
// 'https://me.twonly.eu/r/#7fdb8f08-0927-4e44-8761-038993414e48/1p7SKEzpxE3wSW9FQw60EUI4OpSW2U4EskdXLw8xg48',
// );
setState(() { setState(() {
_isLoaded = true; _isLoaded = true;
}); });
@ -223,9 +242,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
} }
} else if (_showOnboarding) { } else if (_showOnboarding) {
child = OnboardingView( child = OnboardingView(
callbackOnSuccess: () => setState(() { callbackOnSuccess: () async {
_showOnboarding = false; await KeyValueStore.update<OnboardingState>(
}), key: KeyValueKeys.onboardingState,
update: (state) => state.hasOnboardingFinished = true,
);
if (mounted) setState(() => _showOnboarding = false);
},
); );
} else { } else {
child = RegisterView( child = RegisterView(

View file

@ -0,0 +1,173 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../../bridge.dart';
import '../../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`
class UserDiscoveryStoreFlutter {
const UserDiscoveryStoreFlutter();
Future<AnnouncedUser?> getAnnouncedUserByPublicId({
required PlatformInt64 publicId,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetAnnouncedUserByPublicId(
that: this,
publicId: publicId,
);
Future<String> getConfig() => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetConfig(
that: this,
);
Future<Uint8List?> getContactPromotion({
required PlatformInt64 contactId,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetContactPromotion(
that: this,
contactId: contactId,
);
Future<Uint8List?> getContactVersion({
required PlatformInt64 contactId,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetContactVersion(
that: this,
contactId: contactId,
);
Future<List<OtherPromotion>> getOtherPromotionsByPublicId({
required PlatformInt64 publicId,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetOtherPromotionsByPublicId(
that: this,
publicId: publicId,
);
Future<List<Uint8List>> getOwnPromotionsAfterVersion({
required int version,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetOwnPromotionsAfterVersion(
that: this,
version: version,
);
Future<Uint8List> getShareForContact({
required PlatformInt64 contactId,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterGetShareForContact(
that: this,
contactId: contactId,
);
Future<void> pushNewUserRelation({
required PlatformInt64 fromContactId,
required AnnouncedUser announcedUser,
PlatformInt64? publicKeyVerifiedTimestamp,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterPushNewUserRelation(
that: this,
fromContactId: fromContactId,
announcedUser: announcedUser,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
);
Future<void> pushOwnPromotionAndClearOldVersion({
required PlatformInt64 contactId,
required int version,
required List<int> promotion,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterPushOwnPromotionAndClearOldVersion(
that: this,
contactId: contactId,
version: version,
promotion: promotion,
);
Future<void> setContactVersion({
required PlatformInt64 contactId,
required List<int> update,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterSetContactVersion(
that: this,
contactId: contactId,
update: update,
);
Future<void> setShares({required List<Uint8List> shares}) => RustLib
.instance
.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterSetShares(
that: this,
shares: shares,
);
Future<void> storeOtherPromotion({
required OtherPromotion promotion,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterStoreOtherPromotion(
that: this,
promotion: promotion,
);
Future<void> updateConfig({required String update}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryStoreFlutterUpdateConfig(
that: this,
update: update,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserDiscoveryStoreFlutter && runtimeType == other.runtimeType;
}
class UserDiscoveryUtilsFlutter {
const UserDiscoveryUtilsFlutter();
Future<Uint8List> signData({required List<int> inputData}) => RustLib
.instance
.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryUtilsFlutterSignData(
that: this,
inputData: inputData,
);
Future<bool> verifySignature({
required List<int> inputData,
required List<int> pubkey,
required List<int> signature,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryUtilsFlutterVerifySignature(
that: this,
inputData: inputData,
pubkey: pubkey,
signature: signature,
);
Future<bool> verifyStoredPubkey({
required PlatformInt64 fromContactId,
required List<int> pubkey,
}) => RustLib.instance.api
.crateBridgeCallbacksUserDiscoveryUserDiscoveryUtilsFlutterVerifyStoredPubkey(
that: this,
fromContactId: fromContactId,
pubkey: pubkey,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserDiscoveryUtilsFlutter && runtimeType == other.runtimeType;
}

View file

@ -0,0 +1,37 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class RustUtils {
const RustUtils();
static Future<List<Uint8List>> generateShares({
required List<int> secret,
required int total,
required int threshold,
}) => RustLib.instance.api.crateBridgeWrapperRustUtilsGenerateShares(
secret: secret,
total: total,
threshold: threshold,
);
static Future<Uint8List> recoverSecret({
required List<Uint8List> shares,
required int threshold,
}) => RustLib.instance.api.crateBridgeWrapperRustUtilsRecoverSecret(
shares: shares,
threshold: threshold,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RustUtils && runtimeType == other.runtimeType;
}

View file

@ -20,6 +20,12 @@ class RustKeyManager {
static Future<PlatformInt64?> getUserId() => RustLib.instance.api static Future<PlatformInt64?> getUserId() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetUserId(); .crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
static Future<void> importSerialized({required List<int> serializedBytes}) =>
RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerImportSerialized(
serializedBytes: serializedBytes,
);
static Future<void> importSignalIdentity({ static Future<void> importSignalIdentity({
required List<int> identityKeyPairStructure, required List<int> identityKeyPairStructure,
required PlatformInt64 registrationId, required PlatformInt64 registrationId,
@ -53,6 +59,10 @@ class RustKeyManager {
signedPreKeyId: signedPreKeyId, signedPreKeyId: signedPreKeyId,
); );
/// Serialize the key_manager. Needed for the passwordless_recovery feature.
static Future<Uint8List> serialize() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerSerialize();
static Future<void> setUserId({required PlatformInt64 userId}) => RustLib static Future<void> setUserId({required PlatformInt64 userId}) => RustLib
.instance .instance
.api .api

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,8 @@
import 'bridge.dart'; import 'bridge.dart';
import 'bridge/callbacks.dart'; import 'bridge/callbacks.dart';
import 'bridge/callbacks/user_discovery.dart';
import 'bridge/wrapper.dart';
import 'bridge/wrapper/backup.dart'; import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart'; import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart'; import 'bridge/wrapper/user_discovery.dart';
@ -134,6 +136,19 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw); InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
OtherPromotion dco_decode_box_autoadd_other_promotion(dynamic raw);
@protected
UserDiscoveryStoreFlutter dco_decode_box_autoadd_user_discovery_store_flutter(
dynamic raw,
);
@protected
UserDiscoveryUtilsFlutter dco_decode_box_autoadd_user_discovery_utils_flutter(
dynamic raw,
);
@protected @protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw); FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -205,6 +220,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw); RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
RustUtils dco_decode_rust_utils(dynamic raw);
@protected @protected
int dco_decode_u_32(dynamic raw); int dco_decode_u_32(dynamic raw);
@ -217,6 +235,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void dco_decode_unit(dynamic raw); void dco_decode_unit(dynamic raw);
@protected
UserDiscoveryStoreFlutter dco_decode_user_discovery_store_flutter(
dynamic raw,
);
@protected
UserDiscoveryUtilsFlutter dco_decode_user_discovery_utils_flutter(
dynamic raw,
);
@protected @protected
BigInt dco_decode_usize(dynamic raw); BigInt dco_decode_usize(dynamic raw);
@ -266,6 +294,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer); InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
OtherPromotion sse_decode_box_autoadd_other_promotion(
SseDeserializer deserializer,
);
@protected
UserDiscoveryStoreFlutter sse_decode_box_autoadd_user_discovery_store_flutter(
SseDeserializer deserializer,
);
@protected
UserDiscoveryUtilsFlutter sse_decode_box_autoadd_user_discovery_utils_flutter(
SseDeserializer deserializer,
);
@protected @protected
FlutterUserDiscovery sse_decode_flutter_user_discovery( FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -357,6 +400,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer); RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
@protected @protected
int sse_decode_u_32(SseDeserializer deserializer); int sse_decode_u_32(SseDeserializer deserializer);
@ -369,6 +415,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_decode_unit(SseDeserializer deserializer); void sse_decode_unit(SseDeserializer deserializer);
@protected
UserDiscoveryStoreFlutter sse_decode_user_discovery_store_flutter(
SseDeserializer deserializer,
);
@protected
UserDiscoveryUtilsFlutter sse_decode_user_discovery_utils_flutter(
SseDeserializer deserializer,
);
@protected @protected
BigInt sse_decode_usize(SseDeserializer deserializer); BigInt sse_decode_usize(SseDeserializer deserializer);
@ -517,6 +573,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_other_promotion(
OtherPromotion self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_user_discovery_store_flutter(
UserDiscoveryStoreFlutter self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_user_discovery_utils_flutter(
UserDiscoveryUtilsFlutter self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_flutter_user_discovery( void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self, FlutterUserDiscovery self,
@ -634,6 +708,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
@protected @protected
void sse_encode_u_32(int self, SseSerializer serializer); void sse_encode_u_32(int self, SseSerializer serializer);
@ -646,6 +723,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_unit(void self, SseSerializer serializer); void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_user_discovery_store_flutter(
UserDiscoveryStoreFlutter self,
SseSerializer serializer,
);
@protected
void sse_encode_user_discovery_utils_flutter(
UserDiscoveryUtilsFlutter self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_usize(BigInt self, SseSerializer serializer); void sse_encode_usize(BigInt self, SseSerializer serializer);

View file

@ -8,6 +8,8 @@
import 'bridge.dart'; import 'bridge.dart';
import 'bridge/callbacks.dart'; import 'bridge/callbacks.dart';
import 'bridge/callbacks/user_discovery.dart';
import 'bridge/wrapper.dart';
import 'bridge/wrapper/backup.dart'; import 'bridge/wrapper/backup.dart';
import 'bridge/wrapper/key_manager.dart'; import 'bridge/wrapper/key_manager.dart';
import 'bridge/wrapper/user_discovery.dart'; import 'bridge/wrapper/user_discovery.dart';
@ -136,6 +138,19 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
InitConfig dco_decode_box_autoadd_init_config(dynamic raw); InitConfig dco_decode_box_autoadd_init_config(dynamic raw);
@protected
OtherPromotion dco_decode_box_autoadd_other_promotion(dynamic raw);
@protected
UserDiscoveryStoreFlutter dco_decode_box_autoadd_user_discovery_store_flutter(
dynamic raw,
);
@protected
UserDiscoveryUtilsFlutter dco_decode_box_autoadd_user_discovery_utils_flutter(
dynamic raw,
);
@protected @protected
FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw); FlutterUserDiscovery dco_decode_flutter_user_discovery(dynamic raw);
@ -207,6 +222,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
RustKeyManager dco_decode_rust_key_manager(dynamic raw); RustKeyManager dco_decode_rust_key_manager(dynamic raw);
@protected
RustUtils dco_decode_rust_utils(dynamic raw);
@protected @protected
int dco_decode_u_32(dynamic raw); int dco_decode_u_32(dynamic raw);
@ -219,6 +237,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void dco_decode_unit(dynamic raw); void dco_decode_unit(dynamic raw);
@protected
UserDiscoveryStoreFlutter dco_decode_user_discovery_store_flutter(
dynamic raw,
);
@protected
UserDiscoveryUtilsFlutter dco_decode_user_discovery_utils_flutter(
dynamic raw,
);
@protected @protected
BigInt dco_decode_usize(dynamic raw); BigInt dco_decode_usize(dynamic raw);
@ -268,6 +296,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer); InitConfig sse_decode_box_autoadd_init_config(SseDeserializer deserializer);
@protected
OtherPromotion sse_decode_box_autoadd_other_promotion(
SseDeserializer deserializer,
);
@protected
UserDiscoveryStoreFlutter sse_decode_box_autoadd_user_discovery_store_flutter(
SseDeserializer deserializer,
);
@protected
UserDiscoveryUtilsFlutter sse_decode_box_autoadd_user_discovery_utils_flutter(
SseDeserializer deserializer,
);
@protected @protected
FlutterUserDiscovery sse_decode_flutter_user_discovery( FlutterUserDiscovery sse_decode_flutter_user_discovery(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -359,6 +402,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer); RustKeyManager sse_decode_rust_key_manager(SseDeserializer deserializer);
@protected
RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
@protected @protected
int sse_decode_u_32(SseDeserializer deserializer); int sse_decode_u_32(SseDeserializer deserializer);
@ -371,6 +417,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_decode_unit(SseDeserializer deserializer); void sse_decode_unit(SseDeserializer deserializer);
@protected
UserDiscoveryStoreFlutter sse_decode_user_discovery_store_flutter(
SseDeserializer deserializer,
);
@protected
UserDiscoveryUtilsFlutter sse_decode_user_discovery_utils_flutter(
SseDeserializer deserializer,
);
@protected @protected
BigInt sse_decode_usize(SseDeserializer deserializer); BigInt sse_decode_usize(SseDeserializer deserializer);
@ -519,6 +575,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_other_promotion(
OtherPromotion self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_user_discovery_store_flutter(
UserDiscoveryStoreFlutter self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_user_discovery_utils_flutter(
UserDiscoveryUtilsFlutter self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_flutter_user_discovery( void sse_encode_flutter_user_discovery(
FlutterUserDiscovery self, FlutterUserDiscovery self,
@ -636,6 +710,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
@protected @protected
void sse_encode_u_32(int self, SseSerializer serializer); void sse_encode_u_32(int self, SseSerializer serializer);
@ -648,6 +725,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_unit(void self, SseSerializer serializer); void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_user_discovery_store_flutter(
UserDiscoveryStoreFlutter self,
SseSerializer serializer,
);
@protected
void sse_encode_user_discovery_utils_flutter(
UserDiscoveryUtilsFlutter self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_usize(BigInt self, SseSerializer serializer); void sse_encode_usize(BigInt self, SseSerializer serializer);

View file

@ -35,6 +35,6 @@ class AppState {
static bool isInBackgroundTask = false; static bool isInBackgroundTask = false;
static bool allowErrorTrackingViaSentry = false; static bool allowErrorTrackingViaSentry = false;
static bool gotMessageFromServer = false; static bool gotMessageFromServer = false;
static int latestAppVersionId = 116; static int latestAppVersionId = 117;
static bool hasCameraPermissions = false; static bool hasCameraPermissions = false;
} }

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
@ -32,7 +33,7 @@ import 'package:twonly/src/utils/startup_guard.dart';
final _initMutex = Mutex(); final _initMutex = Mutex();
/// This function is used to initialized the absolute minimum so it /// This function is used to initialize the absolute minimum so it
/// can also be used by the backend without the UI was loaded. /// can also be used by the backend without the UI was loaded.
Future<bool> twonlyMinimumInitialization() async { Future<bool> twonlyMinimumInitialization() async {
Log.info('twonlyMinimumInitialization: called'); Log.info('twonlyMinimumInitialization: called');
@ -40,16 +41,13 @@ Future<bool> twonlyMinimumInitialization() async {
lockName: 'init', lockName: 'init',
mutex: _initMutex, mutex: _initMutex,
action: () async { action: () async {
Log.info('twonlyMinimumInitialization: started'); Log.info('twonlyMinimumInitialization started');
setupLocator(); setupLocator();
Log.info('twonlyMinimumInitialization: RustLib.init()');
await RustLib.init(); await RustLib.init();
Log.info('twonlyMinimumInitialization: initFlutterCallbacksForRust()');
await initFlutterCallbacksForRust(); await initFlutterCallbacksForRust();
Log.info('twonlyMinimumInitialization: bridge.initializeTwonlyFlutter()');
try { try {
await bridge.initializeTwonlyFlutter( await bridge.initializeTwonlyFlutter(
config: bridge.InitConfig( config: bridge.InitConfig(
@ -71,12 +69,21 @@ Future<bool> twonlyMinimumInitialization() async {
void main() async { void main() async {
final binding = SentryWidgetsFlutterBinding.ensureInitialized(); final binding = SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init(); await AppEnvironment.init();
// Preload available cameras in the background to speed up camera tab startup
unawaited(
availableCameras().then((cameras) {
AppEnvironment.cameras = cameras;
}),
);
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
unawaited(StartupGuard.markAppStartup()); unawaited(StartupGuard.markAppStartup());
var storageError = await twonlyMinimumInitialization(); var storageError = await twonlyMinimumInitialization();
await initFCMService(); await FcmNotificationService.initStartup();
await setupPushNotification();
var userExists = false; var userExists = false;
@ -109,6 +116,8 @@ void main() async {
unawaited(initFileDownloader()); unawaited(initFileDownloader());
if (userExists) { if (userExists) {
unawaited(FcmNotificationService.initAfterUserLoaded());
if (userService.currentUser.allowErrorTrackingViaSentry) { if (userService.currentUser.allowErrorTrackingViaSentry) {
AppState.allowErrorTrackingViaSentry = true; AppState.allowErrorTrackingViaSentry = true;
await SentryFlutter.init( await SentryFlutter.init(
@ -164,7 +173,6 @@ Future<void> postStartupTasks() async {
unawaited(MediaFileService.purgeTempFolder()); unawaited(MediaFileService.purgeTempFolder());
// 2. Service initializations // 2. Service initializations
unawaited(setupPushNotification());
unawaited(finishStartedPreprocessing()); unawaited(finishStartedPreprocessing());
unawaited(createPushAvatars()); unawaited(createPushAvatars());

View file

@ -314,9 +314,11 @@ class UserDiscoveryCallbacks {
static Future<Uint8List?> getContactPromotion(int contactId) async { static Future<Uint8List?> getContactPromotion(int contactId) async {
try { try {
final row = await (twonlyDB.select( final query = twonlyDB.select(twonlyDB.userDiscoveryOwnPromotions)
twonlyDB.userDiscoveryOwnPromotions, ..where((tbl) => tbl.contactId.equals(contactId))
)..where((tbl) => tbl.contactId.equals(contactId))).getSingleOrNull(); ..orderBy([(tbl) => OrderingTerm.desc(tbl.versionId)])
..limit(1);
final row = await query.getSingleOrNull();
return row?.promotion; return row?.promotion;
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);

View file

@ -36,7 +36,7 @@ abstract class VideoCompressionChannel {
}); });
return outputPath; return outputPath;
} on PlatformException catch (e) { } on PlatformException catch (e) {
Log.error('Failed to compress video: $e'); Log.warn('Failed to compress video: $e');
return null; return null;
} finally { } finally {
_currentProgressCallback = null; _currentProgressCallback = null;

View file

@ -3,4 +3,5 @@ class KeyValueKeys {
'last_periodic_task_execution'; 'last_periodic_task_execution';
static const String currentBackupState = 'current_backup_state'; static const String currentBackupState = 'current_backup_state';
static const String backupRecoveryState = 'backup_recovery_state'; static const String backupRecoveryState = 'backup_recovery_state';
static const String onboardingState = 'onboarding_state';
} }

View file

@ -16,6 +16,7 @@ class Routes {
static String profileContact(int contactId) => '/profile/contact/$contactId'; static String profileContact(int contactId) => '/profile/contact/$contactId';
static const String cameraQRScanner = '/camera/qr_scanner'; static const String cameraQRScanner = '/camera/qr_scanner';
static const String recoverPasswordless = '/recover/passwordless';
static const String settings = '/settings'; static const String settings = '/settings';
static const String settingsProfile = '/settings/profile'; static const String settingsProfile = '/settings/profile';
@ -35,8 +36,6 @@ class Routes {
'/settings/privacy/block_users'; '/settings/privacy/block_users';
static const String settingsPrivacyUserDiscovery = static const String settingsPrivacyUserDiscovery =
'/settings/privacy/user_discovery'; '/settings/privacy/user_discovery';
static const String settingsPrivacyProfileSelection =
'/settings/privacy/profile_selection';
static const String settingsNotification = '/settings/notification'; static const String settingsNotification = '/settings/notification';
static const String settingsStorage = '/settings/storage_data'; static const String settingsStorage = '/settings/storage_data';
static const String settingsStorageManage = '/settings/storage_data/manage'; static const String settingsStorageManage = '/settings/storage_data/manage';

View file

@ -56,6 +56,18 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
return (delete(contacts)..where((t) => t.userId.equals(userId))).go(); return (delete(contacts)..where((t) => t.userId.equals(userId))).go();
} }
Future<void> resetRecoveryDataForAllContacts() async {
await update(
contacts,
).write(
const ContactsCompanion(
recoveryIsTrustedFriend: Value(false),
recoveryLastHeartbeat: Value(null),
recoverySecretShare: Value(null),
),
);
}
Future<void> updateContact( Future<void> updateContact(
int userId, int userId,
ContactsCompanion updatedValues, ContactsCompanion updatedValues,

View file

@ -128,7 +128,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
final result = await _insertGroup(insertGroup); final result = await _insertGroup(insertGroup);
if (result != null) { if (result != null) {
await into(groupMembers).insert( await into(groupMembers).insertOnConflictUpdate(
GroupMembersCompanion( GroupMembersCompanion(
groupId: Value(result.groupId), groupId: Value(result.groupId),
contactId: Value( contactId: Value(

View file

@ -57,18 +57,70 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
} }
Future<bool> isContactVerified(int contactId) async { Future<bool> isContactVerified(int contactId) async {
final row = final verifierKv = alias(keyVerifications, 'verifierKv');
await (select(keyVerifications) final query = select(keyVerifications).join([
..where((kv) => kv.contactId.equals(contactId)) leftOuterJoin(
..limit(1)) verifierKv,
.getSingleOrNull(); verifierKv.contactId.equalsExp(keyVerifications.verifiedBy),
return row != null; ),
])..where(keyVerifications.contactId.equals(contactId));
final rows = await query.get();
for (final row in rows) {
final kv = row.readTable(keyVerifications);
final hasVerifierKv = row.readTableOrNull(verifierKv) != null;
if (kv.type == VerificationType.contactSharedByVerified) {
if (hasVerifierKv) return true;
} else {
return true;
}
}
return false;
} }
Stream<List<KeyVerification>> watchContactVerification(int contactId) { Stream<List<(KeyVerification, Contact?)>> watchContactVerification(
return (select( int contactId,
keyVerifications, ) {
)..where((kv) => kv.contactId.equals(contactId))).watch(); final verifier = alias(contacts, 'verifier');
final verifierKv = alias(keyVerifications, 'verifierKv');
final query = select(keyVerifications).join([
leftOuterJoin(
verifier,
verifier.userId.equalsExp(keyVerifications.verifiedBy),
),
leftOuterJoin(
verifierKv,
verifierKv.contactId.equalsExp(keyVerifications.verifiedBy),
),
])..where(keyVerifications.contactId.equals(contactId));
return query.watch().map((rows) {
final uniqueKvs =
<int, (KeyVerification, Contact?, bool isVerifierVerified)>{};
for (final row in rows) {
final kv = row.readTable(keyVerifications);
final contact = row.readTableOrNull(verifier);
final hasVerifierKv = row.readTableOrNull(verifierKv) != null;
final existing = uniqueKvs[kv.verificationId];
if (existing == null || hasVerifierKv) {
uniqueKvs[kv.verificationId] = (kv, contact, hasVerifierKv);
}
}
return uniqueKvs.values
.where((item) {
final kv = item.$1;
final isVerifierVerified = item.$3;
if (kv.type == VerificationType.contactSharedByVerified) {
return isVerifierVerified;
}
return true;
})
.map((item) => (item.$1, item.$2))
.toList();
});
} }
Future<List<KeyVerification>> getContactVerification(int contactId) async { Future<List<KeyVerification>> getContactVerification(int contactId) async {
@ -207,12 +259,65 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
}); });
} }
Future<void> addKeyVerification(int contactId, VerificationType type) async { Stream<int> watchUnverifiedGroupMembersCount(String groupId) {
final gm = groupMembers;
final directKv = alias(keyVerifications, 'directKv');
final ur = userDiscoveryUserRelations;
final verifierKv = alias(keyVerifications, 'verifierKv');
final query = select(gm).join([
leftOuterJoin(directKv, directKv.contactId.equalsExp(gm.contactId)),
leftOuterJoin(
ur,
ur.announcedUserId.equalsExp(gm.contactId) &
ur.publicKeyVerifiedTimestamp.isNotNull() &
ur.fromContactId.equalsExp(gm.contactId).not(),
),
leftOuterJoin(
verifierKv,
verifierKv.contactId.equalsExp(ur.fromContactId),
),
])..where(gm.groupId.equals(groupId));
return query.watch().map((rows) {
if (rows.isEmpty) return 0;
final memberTrustMap = <int, ({bool direct, bool partial})>{};
for (final row in rows) {
final contactId = row.readTable(gm).contactId;
final isDirect = row.readTableOrNull(directKv) != null;
final isPartial = row.readTableOrNull(verifierKv) != null;
final current =
memberTrustMap[contactId] ?? (direct: false, partial: false);
memberTrustMap[contactId] = (
direct: current.direct || isDirect,
partial: current.partial || isPartial,
);
}
var count = 0;
for (final trust in memberTrustMap.values) {
if (!trust.direct && !trust.partial) {
count++;
}
}
return count;
});
}
Future<void> addKeyVerification(
int contactId,
VerificationType type, {
int? verifiedBy,
}) async {
try { try {
await into(keyVerifications).insertOnConflictUpdate( await into(keyVerifications).insertOnConflictUpdate(
KeyVerificationsCompanion( KeyVerificationsCompanion(
contactId: Value(contactId), contactId: Value(contactId),
type: Value(type), type: Value(type),
verifiedBy: Value(verifiedBy),
), ),
); );
if (userService.currentUser.isUserDiscoveryEnabled) { if (userService.currentUser.isUserDiscoveryEnabled) {

View file

@ -1,12 +1,13 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
part 'mediafiles.dao.g.dart'; part 'mediafiles.dao.g.dart';
@DriftAccessor(tables: [MediaFiles]) @DriftAccessor(tables: [MediaFiles, Messages])
class MediaFilesDao extends DatabaseAccessor<TwonlyDB> class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
with _$MediaFilesDaoMixin { with _$MediaFilesDaoMixin {
// this constructor is required so that the main database can create an instance // this constructor is required so that the main database can create an instance
@ -142,7 +143,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
final query = final query =
(select(mediaFiles)..where((t) => t.stored.equals(true))).join([]) (select(mediaFiles)..where((t) => t.stored.equals(true))).join([])
..groupBy([ ..groupBy([
const CustomExpression<Object>('COALESCE(stored_file_hash, media_id)') const CustomExpression<Object>(
'COALESCE(stored_file_hash, media_id)',
),
]); ]);
return query.map((row) => row.readTable(mediaFiles)).watch(); return query.map((row) => row.readTable(mediaFiles)).watch();
} }
@ -154,6 +157,17 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
.watch(); .watch();
} }
Stream<List<MediaFile>> watchMediaFilesForGroup(String groupId) {
final query = select(mediaFiles).join([
innerJoin(
db.messages,
db.messages.mediaId.equalsExp(mediaFiles.mediaId),
useColumns: false,
),
])..where(db.messages.groupId.equals(groupId));
return query.map((row) => row.readTable(mediaFiles)).watch();
}
Future<void> updateAllRetransmissionUploadingState() async { Future<void> updateAllRetransmissionUploadingState() async {
await (update(mediaFiles)..where( await (update(mediaFiles)..where(
(t) => (t) =>

View file

@ -5,6 +5,9 @@ part of 'mediafiles.dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$MediaFilesDaoMixin on DatabaseAccessor<TwonlyDB> { mixin _$MediaFilesDaoMixin on DatabaseAccessor<TwonlyDB> {
$MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles;
$GroupsTable get groups => attachedDatabase.groups;
$ContactsTable get contacts => attachedDatabase.contacts;
$MessagesTable get messages => attachedDatabase.messages;
MediaFilesDaoManager get managers => MediaFilesDaoManager(this); MediaFilesDaoManager get managers => MediaFilesDaoManager(this);
} }
@ -13,4 +16,10 @@ class MediaFilesDaoManager {
MediaFilesDaoManager(this._db); MediaFilesDaoManager(this._db);
$$MediaFilesTableTableManager get mediaFiles => $$MediaFilesTableTableManager get mediaFiles =>
$$MediaFilesTableTableManager(_db.attachedDatabase, _db.mediaFiles); $$MediaFilesTableTableManager(_db.attachedDatabase, _db.mediaFiles);
$$GroupsTableTableManager get groups =>
$$GroupsTableTableManager(_db.attachedDatabase, _db.groups);
$$ContactsTableTableManager get contacts =>
$$ContactsTableTableManager(_db.attachedDatabase, _db.contacts);
$$MessagesTableTableManager get messages =>
$$MessagesTableTableManager(_db.attachedDatabase, _db.messages);
} }

View file

@ -32,15 +32,25 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
MessagesDao(super.db); MessagesDao(super.db);
Stream<List<Message>> watchMessageNotOpened(String groupId) { Stream<List<Message>> watchMessageNotOpened(String groupId) {
return (select(messages) final query =
select(messages).join([
leftOuterJoin(
mediaFiles,
mediaFiles.mediaId.equalsExp(messages.mediaId),
),
])
..where( ..where(
(t) => messages.openedAt.isNull() &
t.openedAt.isNull() & messages.groupId.equals(groupId) &
t.groupId.equals(groupId) & messages.isDeletedFromSender.equals(false) &
t.isDeletedFromSender.equals(false), (messages.mediaId.isNull() |
mediaFiles.downloadState.isNull() |
mediaFiles.downloadState
.equals(DownloadState.reuploadRequested.name)
.not()),
) )
..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) ..orderBy([OrderingTerm.desc(messages.createdAt)]);
.watch(); return query.map((row) => row.readTable(messages)).watch();
} }
Stream<List<Message>> watchMediaNotOpened(String groupId) { Stream<List<Message>> watchMediaNotOpened(String groupId) {
@ -52,9 +62,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
), ),
]) ])
..where( ..where(
mediaFiles.downloadState (mediaFiles.downloadState.isNull() |
.equals(DownloadState.reuploadRequested.name) mediaFiles.downloadState
.not() & .equals(DownloadState.reuploadRequested.name)
.not()) &
mediaFiles.type.equals(MediaType.audio.name).not() & mediaFiles.type.equals(MediaType.audio.name).not() &
messages.openedAt.isNull() & messages.openedAt.isNull() &
messages.groupId.equals(groupId) & messages.groupId.equals(groupId) &
@ -66,6 +77,24 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
return query.map((row) => row.readTable(messages)).watch(); return query.map((row) => row.readTable(messages)).watch();
} }
Stream<List<MediaFile>> watchUnopenedMediaFiles() {
final query =
select(messages).join([
leftOuterJoin(
mediaFiles,
mediaFiles.mediaId.equalsExp(messages.mediaId),
),
])..where(
messages.openedAt.isNull() &
messages.mediaId.isNotNull() &
messages.type.equals(MessageType.media.name) &
mediaFiles.downloadState.equals(DownloadState.ready.name) &
(mediaFiles.type.equals(MediaType.image.name) |
mediaFiles.type.equals(MediaType.gif.name)),
);
return query.map((row) => row.readTable(mediaFiles)).watch();
}
Future<Stream<Message?>> watchLastMessage(String groupId) async { Future<Stream<Message?>> watchLastMessage(String groupId) async {
final group = await twonlyDB.groupsDao.getGroup(groupId); final group = await twonlyDB.groupsDao.getGroup(groupId);
final deletionTime = clock.now().subtract( final deletionTime = clock.now().subtract(
@ -73,19 +102,29 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
milliseconds: group!.deleteMessagesAfterMilliseconds, milliseconds: group!.deleteMessagesAfterMilliseconds,
), ),
); );
return (select(messages) final query =
select(messages).join([
leftOuterJoin(
mediaFiles,
mediaFiles.mediaId.equalsExp(messages.mediaId),
),
])
..where( ..where(
(t) => messages.groupId.equals(groupId) &
t.groupId.equals(groupId) &
// messages in groups will only be removed in case all members have received it... // messages in groups will only be removed in case all members have received it...
// so ensuring that this message is not shown in the messages anymore // so ensuring that this message is not shown in the messages anymore
(t.openedAt.isBiggerThanValue(deletionTime) | (messages.openedAt.isBiggerThanValue(deletionTime) |
t.openedAt.isNull() | messages.openedAt.isNull() |
t.mediaStored.equals(true)), messages.mediaStored.equals(true)) &
(mediaFiles.downloadState
.equals(DownloadState.reuploadRequested.name)
.not() |
mediaFiles.downloadState.isNull()),
) )
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]) ..orderBy([OrderingTerm.desc(messages.createdAt)])
..limit(1)) ..limit(1);
.watchSingleOrNull();
return query.map((row) => row.readTable(messages)).watchSingleOrNull();
} }
Future<Stream<List<Message>>> watchByGroupId(String groupId) async { Future<Stream<List<Message>>> watchByGroupId(String groupId) async {
@ -245,26 +284,28 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String text, String text,
DateTime timestamp, DateTime timestamp,
) async { ) async {
final msg = await getMessageById(messageId).getSingleOrNull(); await transaction(() async {
if (msg == null || msg.content == null || msg.senderId != contactId) { final msg = await getMessageById(messageId).getSingleOrNull();
return; if (msg == null || msg.content == null || msg.senderId != contactId) {
} return;
await into(messageHistories).insert( }
MessageHistoriesCompanion( await into(messageHistories).insert(
messageId: Value(messageId), MessageHistoriesCompanion(
content: Value(msg.content), messageId: Value(messageId),
createdAt: Value(timestamp), content: Value(msg.content),
), createdAt: Value(timestamp),
); ),
await (update(messages)..where( );
(t) => t.messageId.equals(messageId), await (update(messages)..where(
)) (t) => t.messageId.equals(messageId),
.write( ))
MessagesCompanion( .write(
content: Value(text), MessagesCompanion(
modifiedAt: Value(timestamp), content: Value(text),
), modifiedAt: Value(timestamp),
); ),
);
});
} }
Future<void> handleMessagesOpened( Future<void> handleMessagesOpened(
@ -272,21 +313,37 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
List<String> messageIds, List<String> messageIds,
DateTime timestamp, DateTime timestamp,
) async { ) async {
if (contactId.present) {
final contactExists = await twonlyDB.contactsDao.getContactById(
contactId.value,
);
if (contactExists == null) {
Log.info(
'handleMessagesOpened: Contact ${contactId.value} does not exist in database, ignoring messages opened action.',
);
return;
}
}
for (final messageId in messageIds) { for (final messageId in messageIds) {
try { try {
var actionTimestamp = timestamp;
final msg = await getMessageById(messageId).getSingleOrNull();
if (msg != null && actionTimestamp.isBefore(msg.createdAt)) {
Log.warn(
'Receiver clock skew detected for message $messageId. '
'Action timestamp $actionTimestamp is before message creation ${msg.createdAt}. '
'Clamping to creation time.',
);
actionTimestamp = msg.createdAt;
}
final ts = actionTimestamp;
await transaction(() async { await transaction(() async {
final msg = await getMessageById(messageId).getSingleOrNull();
if (msg == null) {
Log.info(
'handleMessagesOpened: Message $messageId does not exist in database, skipping.',
);
return;
}
var ts = timestamp;
if (ts.isBefore(msg.createdAt)) {
Log.warn(
'Receiver clock skew detected for message $messageId. '
'Action timestamp $ts is before message creation ${msg.createdAt}. '
'Clamping to creation time.',
);
ts = msg.createdAt;
}
await into(messageActions).insertOnConflictUpdate( await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion( MessageActionsCompanion(
messageId: Value(messageId), messageId: Value(messageId),
@ -320,7 +377,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
messages, messages,
)..where((tbl) => tbl.messageId.equals(messageId))).write( )..where((tbl) => tbl.messageId.equals(messageId))).write(
MessagesCompanion( MessagesCompanion(
openedAt: Value(actionTimestamp), openedAt: Value(timestamp),
), ),
); );
} }
@ -329,7 +386,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
'handleMessagesOpened completed for message $messageId', 'handleMessagesOpened completed for message $messageId',
); );
} catch (e) { } catch (e) {
Log.error('handleMessagesOpened failed for $messageId: $e'); Log.warn('handleMessagesOpened failed for $messageId: $e');
Log.error(
'handleMessagesOpened failed for: $e',
onlyIfSentryEnabled: true,
);
} }
} }
} }
@ -339,7 +400,21 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String messageId, String messageId,
DateTime timestamp, DateTime timestamp,
) async { ) async {
final contactExists = await twonlyDB.contactsDao.getContactById(contactId);
if (contactExists == null) {
Log.info(
'handleMessageAckByServer: Contact $contactId does not exist in database, ignoring message ack.',
);
return;
}
await transaction(() async { await transaction(() async {
final msg = await getMessageById(messageId).getSingleOrNull();
if (msg == null) {
Log.info(
'handleMessageAckByServer: Message $messageId does not exist in database, skipping.',
);
return;
}
await into(messageActions).insertOnConflictUpdate( await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion( MessageActionsCompanion(
messageId: Value(messageId), messageId: Value(messageId),
@ -453,6 +528,15 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
.getSingleOrNull(); .getSingleOrNull();
} }
Stream<MessageAction?> watchLastMessageAction(String messageId) {
return (((select(messageActions)..where(
(t) => t.messageId.equals(messageId),
))
..orderBy([(t) => OrderingTerm.desc(t.actionAt)]))
..limit(1))
.watchSingleOrNull();
}
Future<void> deleteMessagesById(String messageId) { Future<void> deleteMessagesById(String messageId) {
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
} }

View file

@ -30,11 +30,15 @@ class ReactionsDao extends DatabaseAccessor<TwonlyDB> with _$ReactionsDaoMixin {
.getMessageById(messageId) .getMessageById(messageId)
.getSingleOrNull(); .getSingleOrNull();
if (msg == null) { if (msg == null) {
Log.error('updateReaction: Message $messageId not found!'); Log.warn('updateReaction: Message $messageId not found!');
return; return;
} }
if (msg.groupId != groupId) { if (msg.groupId != groupId) {
Log.error('updateReaction: Message groupId ${msg.groupId} != $groupId'); Log.warn('updateReaction: Message groupId ${msg.groupId} != $groupId');
Log.error(
'updateReaction: Message groupId mismatch',
onlyIfSentryEnabled: true,
);
return; return;
} }

View file

@ -53,7 +53,7 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
)) ))
.go(); .go();
} }
Future<void> deleteReceiptsByMessageId(String messageId) async { Future<void> deleteReceiptsByMessageId(String messageId) async {
await (delete(receipts)..where( await (delete(receipts)..where(
(t) => t.messageId.equals(messageId), (t) => t.messageId.equals(messageId),
@ -201,8 +201,8 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
); );
final updatedReceipt = await getReceiptById(newReceiptId); final updatedReceipt = await getReceiptById(newReceiptId);
if (updatedReceipt == null) { if (updatedReceipt == null) {
Log.error( Log.warn(
'Tried to change the receipt ID, but could not get the updated receipt...', '[$oldReceiptId] Tried to change the receipt ID to $newReceiptId, but could not get the updated receipt...',
); );
} }
return updatedReceipt; return updatedReceipt;
@ -253,6 +253,9 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
Future<void> gotReceipt(String receiptId) async { Future<void> gotReceipt(String receiptId) async {
await into( await into(
receivedReceipts, receivedReceipts,
).insert(ReceivedReceiptsCompanion(receiptId: Value(receiptId))); ).insert(
ReceivedReceiptsCompanion(receiptId: Value(receiptId)),
mode: InsertMode.insertOrIgnore,
);
} }
} }

View file

@ -1,4 +1,5 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart'; import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -184,23 +185,51 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
results[user]!.add(relationData); results[user]!.add(relationData);
} }
final threshold = userService.currentUser.userDiscoveryThreshold;
results.removeWhere((user, relations) => relations.length < threshold);
return results; return results;
}); });
} }
Stream<int> watchNewAnnouncementsWithDataCount() { Stream<int> watchNewAnnouncementsWithDataCount() {
final countExp = userDiscoveryAnnouncedUsers.announcedUserId.count(); final announcedContact = alias(contacts, 'announcedContact');
final query =
select(userDiscoveryAnnouncedUsers).join([
innerJoin(
userDiscoveryUserRelations,
userDiscoveryUserRelations.announcedUserId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
leftOuterJoin(
announcedContact,
announcedContact.userId.equalsExp(
userDiscoveryAnnouncedUsers.announcedUserId,
),
),
])..where(
// Filters: Has a username AND has not been shown to the user yet AND is not an existing contact
userDiscoveryAnnouncedUsers.username.isNotNull() &
userDiscoveryAnnouncedUsers.wasShownToTheUser.equals(false) &
userDiscoveryAnnouncedUsers.isHidden.equals(false) &
(announcedContact.userId.isNull() |
announcedContact.deletedByUser.equals(true)),
);
final query = selectOnly(userDiscoveryAnnouncedUsers) return query.watch().map((rows) {
..addColumns([countExp]) final relationCounts = <int, int>{};
..where( for (final row in rows) {
// Filters: Has a username AND has not been shown to the user yet final announcedUserId = row
userDiscoveryAnnouncedUsers.username.isNotNull() & .readTable(userDiscoveryAnnouncedUsers)
userDiscoveryAnnouncedUsers.wasShownToTheUser.equals(false) & .announcedUserId;
userDiscoveryAnnouncedUsers.isHidden.equals(false), relationCounts[announcedUserId] =
); (relationCounts[announcedUserId] ?? 0) + 1;
}
return query.watchSingle().map((row) => row.read(countExp) ?? 0); final threshold = userService.currentUser.userDiscoveryThreshold;
return relationCounts.values.where((count) => count >= threshold).length;
});
} }
Future<void> markAllValidAnnouncedUsersAsShown() async { Future<void> markAllValidAnnouncedUsersAsShown() async {
@ -234,6 +263,12 @@ class UserDiscoveryDao extends DatabaseAccessor<TwonlyDB>
)..where((tbl) => tbl.announcedUserId.equals(id))).getSingleOrNull(); )..where((tbl) => tbl.announcedUserId.equals(id))).getSingleOrNull();
} }
Stream<UserDiscoveryAnnouncedUser?> watchAnnouncedUser(int id) {
return (select(
userDiscoveryAnnouncedUsers,
)..where((tbl) => tbl.announcedUserId.equals(id))).watchSingleOrNull();
}
Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() => Stream<List<UserDiscoveryAnnouncedUser>> watchAllAnnouncedUsers() =>
select(userDiscoveryAnnouncedUsers).watch(); select(userDiscoveryAnnouncedUsers).watch();

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -42,7 +42,9 @@ class SignalPreKeyStore extends PreKeyStore {
); );
try { try {
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion); await twonlyDB
.into(twonlyDB.signalPreKeyStores)
.insert(preKeyCompanion, mode: InsertMode.insertOrReplace);
} catch (e) { } catch (e) {
Log.error('$e'); Log.error('$e');
} }

View file

@ -23,15 +23,26 @@ class Contacts extends Table {
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
// contact_versions: HashMap<UserID, Vec<u8>>, // User Discovery
BlobColumn get userDiscoveryVersion => blob().nullable()(); BlobColumn get userDiscoveryVersion => blob().nullable()();
BoolColumn get userDiscoveryExcluded => BoolColumn get userDiscoveryExcluded =>
boolean().withDefault(const Constant(false))(); boolean().withDefault(const Constant(false))();
BoolColumn get userDiscoveryManualApproved => BoolColumn get userDiscoveryManualApproved =>
boolean().nullable().withDefault(const Constant(false))(); boolean().nullable().withDefault(const Constant(false))();
// Passwordless-Recovery
BoolColumn get recoveryIsTrustedFriend =>
boolean().withDefault(const Constant(false))();
DateTimeColumn get recoveryLastHeartbeat => dateTime().nullable()();
BlobColumn get recoverySecretShare => blob().nullable()();
// This is the share from the contact in case the contact has selected this user as his trusted friend.
BlobColumn get recoveryContactsSecretShare => blob().nullable()();
DateTimeColumn get recoveryContactsLastHeartbeat => dateTime().nullable()();
IntColumn get recoveryContactsThreshold => integer().nullable()();
BoolColumn get askForFriendPromotions => boolean().nullable()();
IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))(); IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))();
IntColumn get mediaReceivedCounter => IntColumn get mediaReceivedCounter =>
integer().withDefault(const Constant(0))(); integer().withDefault(const Constant(0))();
@ -57,6 +68,11 @@ class KeyVerifications extends Table {
onDelete: KeyAction.cascade, onDelete: KeyAction.cascade,
)(); )();
TextColumn get type => textEnum<VerificationType>()(); TextColumn get type => textEnum<VerificationType>()();
IntColumn get verifiedBy => integer().nullable().references(
Contacts,
#userId,
onDelete: KeyAction.cascade,
)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
} }

View file

@ -82,7 +82,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 17; int get schemaVersion => 22;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
final connection = driftDatabase( final connection = driftDatabase(
@ -233,6 +233,48 @@ class TwonlyDB extends _$TwonlyDB {
schema.userDiscoveryAnnouncedUsers.wasAskedFriends, schema.userDiscoveryAnnouncedUsers.wasAskedFriends,
); );
}, },
from17To18: (m, schema) async {
await m.addColumn(
schema.contacts,
schema.contacts.askForFriendPromotions,
);
},
from18To19: (m, schema) async {
await m.addColumn(
schema.keyVerifications,
schema.keyVerifications.verifiedBy,
);
},
from19To20: (m, schema) async {
await m.addColumn(
schema.contacts,
schema.contacts.recoveryIsTrustedFriend,
);
await m.addColumn(
schema.contacts,
schema.contacts.recoveryLastHeartbeat,
);
await m.addColumn(
schema.contacts,
schema.contacts.recoverySecretShare,
);
},
from20To21: (m, schema) async {
await m.addColumn(
schema.contacts,
schema.contacts.recoveryContactsSecretShare,
);
await m.addColumn(
schema.contacts,
schema.contacts.recoveryContactsLastHeartbeat,
);
},
from21To22: (m, schema) async {
await m.addColumn(
schema.contacts,
schema.contacts.recoveryContactsThreshold,
);
},
)(m, from, to); )(m, from, to);
}, },
); );

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -140,18 +140,6 @@ abstract class AppLocalizations {
/// **'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'** /// **'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'**
String get onboardingFocusBody; String get onboardingFocusBody;
/// No description provided for @onboardingSendTwonliesTitle.
///
/// In en, this message translates to:
/// **'Send twonlies'**
String get onboardingSendTwonliesTitle;
/// No description provided for @onboardingSendTwonliesBody.
///
/// In en, this message translates to:
/// **'Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!'**
String get onboardingSendTwonliesBody;
/// No description provided for @onboardingNotProductTitle. /// No description provided for @onboardingNotProductTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -161,15 +149,9 @@ abstract class AppLocalizations {
/// No description provided for @onboardingNotProductBody. /// No description provided for @onboardingNotProductBody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'twonly is financed by donations and an optional subscription. Your data will never be sold.'** /// **'twonly is financed by an optional subscription. Your data will never be sold.'**
String get onboardingNotProductBody; String get onboardingNotProductBody;
/// No description provided for @onboardingGetStartedTitle.
///
/// In en, this message translates to:
/// **'Let\'s go!'**
String get onboardingGetStartedTitle;
/// No description provided for @registerUsernameSlogan. /// No description provided for @registerUsernameSlogan.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -320,24 +302,12 @@ abstract class AppLocalizations {
/// **'Username not found'** /// **'Username not found'**
String get searchUsernameNotFound; String get searchUsernameNotFound;
/// No description provided for @searchUsernameNotFoundBody.
///
/// In en, this message translates to:
/// **'There is no user with the username \"{username}\" registered'**
String searchUsernameNotFoundBody(Object username);
/// No description provided for @searchUsernameNewFollowerTitle. /// No description provided for @searchUsernameNewFollowerTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Open requests'** /// **'Open requests'**
String get searchUsernameNewFollowerTitle; String get searchUsernameNewFollowerTitle;
/// No description provided for @chatListViewSearchUserNameBtn.
///
/// In en, this message translates to:
/// **'Add your first twonly contact!'**
String get chatListViewSearchUserNameBtn;
/// No description provided for @chatListDetailInput. /// No description provided for @chatListDetailInput.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -512,12 +482,6 @@ abstract class AppLocalizations {
/// **'Store in Gallery'** /// **'Store in Gallery'**
String get settingsStorageDataStoreInGTitle; String get settingsStorageDataStoreInGTitle;
/// No description provided for @settingsStorageDataStoreInGSubtitle.
///
/// In en, this message translates to:
/// **'Store saved images additional in the systems gallery.'**
String get settingsStorageDataStoreInGSubtitle;
/// No description provided for @settingsStorageDataMediaAutoDownload. /// No description provided for @settingsStorageDataMediaAutoDownload.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -611,13 +575,13 @@ abstract class AppLocalizations {
/// No description provided for @settingsPrivacyBlockUsers. /// No description provided for @settingsPrivacyBlockUsers.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Block users'** /// **'Block contacts'**
String get settingsPrivacyBlockUsers; String get settingsPrivacyBlockUsers;
/// No description provided for @settingsPrivacyBlockUsersDesc. /// No description provided for @settingsPrivacyBlockUsersDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.'** /// **'Blocked contacts will not be able to communicate with you. You can unblock a blocked contact at any time.'**
String get settingsPrivacyBlockUsersDesc; String get settingsPrivacyBlockUsersDesc;
/// No description provided for @settingsPrivacyBlockUsersCount. /// No description provided for @settingsPrivacyBlockUsersCount.
@ -626,54 +590,6 @@ abstract class AppLocalizations {
/// **'{len} contact(s)'** /// **'{len} contact(s)'**
String settingsPrivacyBlockUsersCount(Object len); String settingsPrivacyBlockUsersCount(Object len);
/// No description provided for @settingsPrivacyProfileSelectionTitle.
///
/// In en, this message translates to:
/// **'Security Profile'**
String get settingsPrivacyProfileSelectionTitle;
/// No description provided for @settingsPrivacyProfileSelectionDesc.
///
/// In en, this message translates to:
/// **'Choose your setup path and security configuration'**
String get settingsPrivacyProfileSelectionDesc;
/// No description provided for @securityProfileTitle.
///
/// In en, this message translates to:
/// **'Security Profile'**
String get securityProfileTitle;
/// No description provided for @securityProfileSubtitle.
///
/// In en, this message translates to:
/// **'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.'**
String get securityProfileSubtitle;
/// No description provided for @securityProfileNormalTitle.
///
/// In en, this message translates to:
/// **'Normal Protection'**
String get securityProfileNormalTitle;
/// No description provided for @securityProfileNormalDesc.
///
/// In en, this message translates to:
/// **'Good balance between a convenient mode without bothering you too much.'**
String get securityProfileNormalDesc;
/// No description provided for @securityProfileStrictTitle.
///
/// In en, this message translates to:
/// **'Strict Protection'**
String get securityProfileStrictTitle;
/// No description provided for @securityProfileStrictDesc.
///
/// In en, this message translates to:
/// **'Maximum anti-phishing protection but may be inconvenient.'**
String get securityProfileStrictDesc;
/// No description provided for @settingsNotification. /// No description provided for @settingsNotification.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -935,21 +851,63 @@ abstract class AppLocalizations {
/// No description provided for @contactVerifyNumberTitle. /// No description provided for @contactVerifyNumberTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Verify contact'** /// **'Verify contacts'**
String get contactVerifyNumberTitle; String get contactVerifyNumberTitle;
/// No description provided for @verifyUserIdentity.
///
/// In en, this message translates to:
/// **'Verify {username}\'s identity'**
String verifyUserIdentity(Object username);
/// No description provided for @contactVerifyNumberSubtitle.
///
/// In en, this message translates to:
/// **'Verify the identity of your contacts to make sure you are texting the right person.'**
String get contactVerifyNumberSubtitle;
/// No description provided for @inChatContactNotVerified.
///
/// In en, this message translates to:
/// **'Contact not verified.'**
String get inChatContactNotVerified;
/// No description provided for @groupMembersNotVerified.
///
/// In en, this message translates to:
/// **'{count} members are not verified.'**
String groupMembersNotVerified(Object count);
/// No description provided for @userVerifiedTitle. /// No description provided for @userVerifiedTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'User verified'** /// **'Contact verified'**
String get userVerifiedTitle; String get userVerifiedTitle;
/// No description provided for @scanUserQrCode.
///
/// In en, this message translates to:
/// **'Scan {username}\'s QR code'**
String scanUserQrCode(Object username);
/// No description provided for @openOwnQrCode.
///
/// In en, this message translates to:
/// **'Open your own QR code'**
String get openOwnQrCode;
/// No description provided for @contactVerifiedBy. /// No description provided for @contactVerifiedBy.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Verified by {username}'** /// **'Verified by {username}'**
String contactVerifiedBy(Object username); String contactVerifiedBy(Object username);
/// No description provided for @contactSharedByUnknown.
///
/// In en, this message translates to:
/// **'Shared by a verified contact (username not available)'**
String get contactSharedByUnknown;
/// No description provided for @verificationTypeQrScanned. /// No description provided for @verificationTypeQrScanned.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1013,13 +971,13 @@ abstract class AppLocalizations {
/// No description provided for @contactBlockBody. /// No description provided for @contactBlockBody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'A blocked user will no longer be able to send you messages and their profile will be hidden from view. To unblock a user, simply navigate to Settings > Privacy > Blocked Users.'** /// **'A blocked contact will no longer be able to send you messages and their profile will be hidden from view. To unblock a contact, simply navigate to Settings > Privacy > Blocked Contacts.'**
String get contactBlockBody; String get contactBlockBody;
/// No description provided for @contactRemove. /// No description provided for @contactRemove.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Remove user'** /// **'Remove contact'**
String get contactRemove; String get contactRemove;
/// No description provided for @contactRemoveTitle. /// No description provided for @contactRemoveTitle.
@ -1031,7 +989,7 @@ abstract class AppLocalizations {
/// No description provided for @contactRemoveBody. /// No description provided for @contactRemoveBody.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.'** /// **'Permanently remove the contact. If the contact tries to send you a new message, you will have to accept the contact again first.'**
String get contactRemoveBody; String get contactRemoveBody;
/// No description provided for @undo. /// No description provided for @undo.
@ -1634,6 +1592,18 @@ abstract class AppLocalizations {
/// **'Change password'** /// **'Change password'**
String get backupChangePassword; String get backupChangePassword;
/// No description provided for @backupChangePasswordAuthReason.
///
/// In en, this message translates to:
/// **'Changing backup password'**
String get backupChangePasswordAuthReason;
/// No description provided for @backupChangePasswordAuthFailed.
///
/// In en, this message translates to:
/// **'You can only change your password after you have authenticated!'**
String get backupChangePasswordAuthFailed;
/// No description provided for @twonlySafeRecoverTitle. /// No description provided for @twonlySafeRecoverTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -1721,7 +1691,7 @@ abstract class AppLocalizations {
/// No description provided for @reportUser. /// No description provided for @reportUser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Report user'** /// **'Report contact'**
String get reportUser; String get reportUser;
/// No description provided for @newDeviceRegistered. /// No description provided for @newDeviceRegistered.
@ -1958,6 +1928,12 @@ abstract class AppLocalizations {
/// **'Already in Group'** /// **'Already in Group'**
String get alreadyInGroup; String get alreadyInGroup;
/// No description provided for @contactNotVerified.
///
/// In en, this message translates to:
/// **'Not verified'**
String get contactNotVerified;
/// No description provided for @removeContactFromGroupTitle. /// No description provided for @removeContactFromGroupTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2360,18 +2336,6 @@ abstract class AppLocalizations {
/// **'Draft'** /// **'Draft'**
String get draftMessage; String get draftMessage;
/// No description provided for @exportMemories.
///
/// In en, this message translates to:
/// **'Export memories (Beta)'**
String get exportMemories;
/// No description provided for @importMemories.
///
/// In en, this message translates to:
/// **'Import memories (Beta)'**
String get importMemories;
/// No description provided for @voiceMessageSlideToCancel. /// No description provided for @voiceMessageSlideToCancel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2408,6 +2372,12 @@ abstract class AppLocalizations {
/// **'Let a friend scan this QR code to add you'** /// **'Let a friend scan this QR code to add you'**
String get addContactQrSheetSubtext; String get addContactQrSheetSubtext;
/// No description provided for @letUserScanQrCode.
///
/// In en, this message translates to:
/// **'Let {username} scan this QR code'**
String letUserScanQrCode(Object username);
/// No description provided for @finishSetupCardTitle. /// No description provided for @finishSetupCardTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2615,7 +2585,7 @@ abstract class AppLocalizations {
/// No description provided for @verifiedPublicKey. /// No description provided for @verifiedPublicKey.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'The public key of {username} has been verified and is valid.'** /// **'The identity of {username} has been successfully verified.'**
String verifiedPublicKey(Object username); String verifiedPublicKey(Object username);
/// No description provided for @memoriesAYearAgo. /// No description provided for @memoriesAYearAgo.
@ -2765,13 +2735,13 @@ abstract class AppLocalizations {
/// No description provided for @verificationBadgeGeneralDesc. /// No description provided for @verificationBadgeGeneralDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.'** /// **'The badge gives you the peace of mind that you are messaging the *right person* and your *messages remain confidential*.'**
String get verificationBadgeGeneralDesc; String get verificationBadgeGeneralDesc;
/// No description provided for @verificationBadgeGreenDesc. /// No description provided for @verificationBadgeGreenDesc.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'A contact you have *personally* verified.'** /// **'A contact you have *personally verified* using the QR code.'**
String get verificationBadgeGreenDesc; String get verificationBadgeGreenDesc;
/// No description provided for @verificationBadgeYellowDesc. /// No description provided for @verificationBadgeYellowDesc.
@ -2786,6 +2756,36 @@ abstract class AppLocalizations {
/// **'A contact whose identity has *not* yet been verified.'** /// **'A contact whose identity has *not* yet been verified.'**
String get verificationBadgeRedDesc; String get verificationBadgeRedDesc;
/// No description provided for @scanNow.
///
/// In en, this message translates to:
/// **'Scan now'**
String get scanNow;
/// No description provided for @qrScannerVerifyHint.
///
/// In en, this message translates to:
/// **'To verify, the other person must open their QR code (Chat Lists > QR code button at the bottom right)'**
String get qrScannerVerifyHint;
/// No description provided for @qrScannerVerifyUserHint.
///
/// In en, this message translates to:
/// **'To verify, {username} must open their QR code (Chat Lists > QR code button at the bottom right)'**
String qrScannerVerifyUserHint(Object username);
/// No description provided for @openQrCode.
///
/// In en, this message translates to:
/// **'Open QR code'**
String get openQrCode;
/// No description provided for @letFriendScanQrToVerify.
///
/// In en, this message translates to:
/// **'Let a friend scan this QR code to verify you'**
String get letFriendScanQrToVerify;
/// No description provided for @deleteVerificationTitle. /// No description provided for @deleteVerificationTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2804,18 +2804,18 @@ abstract class AppLocalizations {
/// **'{username} has scanned your QR code and is now verified.'** /// **'{username} has scanned your QR code and is now verified.'**
String secretQrTokenVerifiedSnackbar(Object username); String secretQrTokenVerifiedSnackbar(Object username);
/// No description provided for @askForFriendPromotionsPrompt.
///
/// In en, this message translates to:
/// **'Help {username} find familiar faces on twonly by sharing mutual friends.'**
String askForFriendPromotionsPrompt(Object username);
/// No description provided for @mutualGroupsTitle. /// No description provided for @mutualGroupsTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'{count, plural, =1{1 mutual group} other{{count} mutual groups}}'** /// **'{count, plural, =1{1 mutual group} other{{count} mutual groups}}'**
String mutualGroupsTitle(num count); String mutualGroupsTitle(num count);
/// No description provided for @mutualGroupsSentMessages.
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 message sent} other{{count} messages sent}}'**
String mutualGroupsSentMessages(num count);
/// No description provided for @chatEntryFlameRestored. /// No description provided for @chatEntryFlameRestored.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2888,12 +2888,6 @@ abstract class AppLocalizations {
/// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'** /// **'When the typing indicator is turned off, you can\'t see when others are typing a message.'**
String get settingsTypingIndicationSubtitle; String get settingsTypingIndicationSubtitle;
/// No description provided for @scanQrOrShow.
///
/// In en, this message translates to:
/// **'Scan / Show QR'**
String get scanQrOrShow;
/// No description provided for @contactActionBlock. /// No description provided for @contactActionBlock.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -2948,12 +2942,6 @@ abstract class AppLocalizations {
/// **'Mutual Friends'** /// **'Mutual Friends'**
String get userDiscoverySettingsTitle; String get userDiscoverySettingsTitle;
/// No description provided for @userDiscoveryWhyThisIsUsed.
///
/// In en, this message translates to:
/// **'Why this is used'**
String get userDiscoveryWhyThisIsUsed;
/// No description provided for @userDiscoveryFeatureOffers. /// No description provided for @userDiscoveryFeatureOffers.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3236,12 +3224,6 @@ abstract class AppLocalizations {
/// **'Back'** /// **'Back'**
String get back; String get back;
/// No description provided for @onboardingExampleLabel.
///
/// In en, this message translates to:
/// **'Example'**
String get onboardingExampleLabel;
/// No description provided for @makerChangedUsername. /// No description provided for @makerChangedUsername.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3410,12 +3392,6 @@ abstract class AppLocalizations {
/// **'Drag to Zoom'** /// **'Drag to Zoom'**
String get dragToZoom; String get dragToZoom;
/// No description provided for @showUsername.
///
/// In en, this message translates to:
/// **'Show username'**
String get showUsername;
/// No description provided for @onboardingProfileSelectionTitle. /// No description provided for @onboardingProfileSelectionTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3642,30 +3618,6 @@ abstract class AppLocalizations {
/// **'There are no images on your device.'** /// **'There are no images on your device.'**
String get importGalleryNoImagesFoundDesc; String get importGalleryNoImagesFoundDesc;
/// No description provided for @importGalleryShowAllImages.
///
/// In en, this message translates to:
/// **'Show all images'**
String get importGalleryShowAllImages;
/// No description provided for @importGalleryShowTwonlyAlbum.
///
/// In en, this message translates to:
/// **'Show twonly album'**
String get importGalleryShowTwonlyAlbum;
/// No description provided for @importGalleryToggleDescAll.
///
/// In en, this message translates to:
/// **'Viewing all images on your device.'**
String get importGalleryToggleDescAll;
/// No description provided for @importGalleryToggleDescTwonly.
///
/// In en, this message translates to:
/// **'Viewing the \"twonly\" album.'**
String get importGalleryToggleDescTwonly;
/// No description provided for @importGalleryFilterTwonly. /// No description provided for @importGalleryFilterTwonly.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@ -3731,6 +3683,456 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Reset'** /// **'Reset'**
String get avatarCustomizeReset; String get avatarCustomizeReset;
/// No description provided for @passwordlessRecovery.
///
/// In en, this message translates to:
/// **'Passwordless Recovery'**
String get passwordlessRecovery;
/// No description provided for @passwordlessRecoveryNotConfigured.
///
/// In en, this message translates to:
/// **'Not configured'**
String get passwordlessRecoveryNotConfigured;
/// No description provided for @passwordlessRecoveryTestPin.
///
/// In en, this message translates to:
/// **'Test PIN'**
String get passwordlessRecoveryTestPin;
/// No description provided for @passwordlessRecoveryTestPinTitle.
///
/// In en, this message translates to:
/// **'Test PIN'**
String get passwordlessRecoveryTestPinTitle;
/// No description provided for @passwordlessRecoveryTestPinHint.
///
/// In en, this message translates to:
/// **'Enter your PIN'**
String get passwordlessRecoveryTestPinHint;
/// No description provided for @passwordlessRecoveryTestPinCorrect.
///
/// In en, this message translates to:
/// **'PIN is correct!'**
String get passwordlessRecoveryTestPinCorrect;
/// No description provided for @passwordlessRecoveryTestPinIncorrect.
///
/// In en, this message translates to:
/// **'Incorrect PIN.'**
String get passwordlessRecoveryTestPinIncorrect;
/// No description provided for @passwordlessRecoveryTest.
///
/// In en, this message translates to:
/// **'Test'**
String get passwordlessRecoveryTest;
/// No description provided for @passwordlessRecoverySecondFactorNone.
///
/// In en, this message translates to:
/// **'None'**
String get passwordlessRecoverySecondFactorNone;
/// No description provided for @passwordlessRecoverySecondFactorEmailLabel.
///
/// In en, this message translates to:
/// **'Email ({email})'**
String passwordlessRecoverySecondFactorEmailLabel(Object email);
/// No description provided for @passwordlessRecoverySecondFactorPin.
///
/// In en, this message translates to:
/// **'PIN'**
String get passwordlessRecoverySecondFactorPin;
/// No description provided for @passwordlessRecoverySecondFactorEmail.
///
/// In en, this message translates to:
/// **'Email'**
String get passwordlessRecoverySecondFactorEmail;
/// No description provided for @passwordlessRecoveryModify.
///
/// In en, this message translates to:
/// **'Modify Recovery Settings'**
String get passwordlessRecoveryModify;
/// No description provided for @passwordlessRecoveryModifyDesc.
///
/// In en, this message translates to:
/// **'Update your recovery configuration'**
String get passwordlessRecoveryModifyDesc;
/// No description provided for @passwordlessRecoverySecondFactor.
///
/// In en, this message translates to:
/// **'Second Factor'**
String get passwordlessRecoverySecondFactor;
/// No description provided for @passwordlessRecoveryNoFriendsFound.
///
/// In en, this message translates to:
/// **'No trusted friends found.'**
String get passwordlessRecoveryNoFriendsFound;
/// No description provided for @passwordlessRecoveryActiveFriends.
///
/// In en, this message translates to:
/// **'Active Friends'**
String get passwordlessRecoveryActiveFriends;
/// No description provided for @passwordlessRecoveryActiveFriendsDesc.
///
/// In en, this message translates to:
/// **'These trusted friends are actively using twonly and can probably help you recover your account.'**
String get passwordlessRecoveryActiveFriendsDesc;
/// No description provided for @passwordlessRecoveryInactiveFriends.
///
/// In en, this message translates to:
/// **'Inactive Friends'**
String get passwordlessRecoveryInactiveFriends;
/// No description provided for @passwordlessRecoveryInactiveFriendsDesc.
///
/// In en, this message translates to:
/// **'These friends either have not yet received their share or do not use twonly actively anymore, which could mean that they maybe cannot help.'**
String get passwordlessRecoveryInactiveFriendsDesc;
/// No description provided for @passwordlessRecoveryNotEnoughFriends.
///
/// In en, this message translates to:
/// **'Not enough friends selected. You need at least 3.'**
String get passwordlessRecoveryNotEnoughFriends;
/// No description provided for @passwordlessRecoveryLoading.
///
/// In en, this message translates to:
/// **'Loading...'**
String get passwordlessRecoveryLoading;
/// No description provided for @passwordlessRecoveryEnableSuccess.
///
/// In en, this message translates to:
/// **'Passwordless recovery successfully enabled!'**
String get passwordlessRecoveryEnableSuccess;
/// No description provided for @passwordlessRecoveryEnableFailed.
///
/// In en, this message translates to:
/// **'Failed to enable passwordless recovery.'**
String get passwordlessRecoveryEnableFailed;
/// No description provided for @passwordlessRecoveryNeedAtLeast3.
///
/// In en, this message translates to:
/// **'You need at least 3 trusted friends.'**
String get passwordlessRecoveryNeedAtLeast3;
/// No description provided for @passwordlessRecoveryInvalidPin.
///
/// In en, this message translates to:
/// **'Invalid PIN'**
String get passwordlessRecoveryInvalidPin;
/// No description provided for @passwordlessRecoveryEnterPin.
///
/// In en, this message translates to:
/// **'Please enter a PIN.'**
String get passwordlessRecoveryEnterPin;
/// No description provided for @passwordlessRecoveryPinMinLength.
///
/// In en, this message translates to:
/// **'PIN must be at least 4 digits.'**
String get passwordlessRecoveryPinMinLength;
/// No description provided for @passwordlessRecoveryEnterEmail.
///
/// In en, this message translates to:
/// **'Please enter an email address.'**
String get passwordlessRecoveryEnterEmail;
/// No description provided for @passwordlessRecoveryEnableBtn.
///
/// In en, this message translates to:
/// **'Enable Passwordless Recovery'**
String get passwordlessRecoveryEnableBtn;
/// No description provided for @passwordlessRecoveryRecoverBtn.
///
/// In en, this message translates to:
/// **'Recover passwordless'**
String get passwordlessRecoveryRecoverBtn;
/// No description provided for @passwordlessRecoveryModifyBtn.
///
/// In en, this message translates to:
/// **'Modify Passwordless Recovery'**
String get passwordlessRecoveryModifyBtn;
/// No description provided for @passwordlessRecoveryStatusEnabled.
///
/// In en, this message translates to:
/// **'Enabled • {count} trusted friends'**
String passwordlessRecoveryStatusEnabled(num count);
/// No description provided for @passwordlessRecoveryInfoHowItWorks.
///
/// In en, this message translates to:
/// **'How it works'**
String get passwordlessRecoveryInfoHowItWorks;
/// No description provided for @passwordlessRecoveryInfoHowItWorksDesc.
///
/// In en, this message translates to:
/// **'Because twonly operates without central user accounts or phone numbers to maximize privacy, we rely on a decentralized recovery mechanism. Using Shamir\'s Secret Sharing, your cryptographic identity is split into independent shares and distributed among your trusted friends. To restore access, a predefined threshold of these friends must combine their shares.'**
String get passwordlessRecoveryInfoHowItWorksDesc;
/// No description provided for @passwordlessRecoveryInfoWhySecondFactor.
///
/// In en, this message translates to:
/// **'Why a Second Factor?'**
String get passwordlessRecoveryInfoWhySecondFactor;
/// No description provided for @passwordlessRecoveryInfoWhySecondFactorDesc.
///
/// In en, this message translates to:
/// **'The second factor (Email or PIN) serves as a vital cryptographic safeguard against malicious collusion. If your trusted friends were to coordinate their shares behind your back, they still wouldn\'t be able to decrypt your identity without the second factor key.'**
String get passwordlessRecoveryInfoWhySecondFactorDesc;
/// No description provided for @passwordlessRecoveryInfoGotIt.
///
/// In en, this message translates to:
/// **'Got it'**
String get passwordlessRecoveryInfoGotIt;
/// No description provided for @passwordlessRecoveryMethod.
///
/// In en, this message translates to:
/// **'Second factor method'**
String get passwordlessRecoveryMethod;
/// No description provided for @passwordlessRecoveryMethodNoneDesc.
///
/// In en, this message translates to:
/// **'Without second-factor, your friends could collaborate to recover your account. Therefore, it is recommended to configure a second-factor.'**
String get passwordlessRecoveryMethodNoneDesc;
/// No description provided for @passwordlessRecoveryMethodPinHint.
///
/// In en, this message translates to:
/// **'Enter PIN'**
String get passwordlessRecoveryMethodPinHint;
/// No description provided for @passwordlessRecoveryMethodEmailHint.
///
/// In en, this message translates to:
/// **'Enter recovery email address'**
String get passwordlessRecoveryMethodEmailHint;
/// No description provided for @passwordlessRecoveryMethodEmailDesc.
///
/// In en, this message translates to:
/// **'Your email address is *never stored on the server* and is only sent to it in the event of a recovery.'**
String get passwordlessRecoveryMethodEmailDesc;
/// No description provided for @passwordlessRecoveryThresholdDesc.
///
/// In en, this message translates to:
/// **'To recover your account you need {count} of your selected trusted friends.'**
String passwordlessRecoveryThresholdDesc(num count);
/// No description provided for @passwordlessRecoveryThresholdTitle.
///
/// In en, this message translates to:
/// **'Required trusted friends for recovery'**
String get passwordlessRecoveryThresholdTitle;
/// No description provided for @passwordlessRecoverySelectFriendsNeeded.
///
/// In en, this message translates to:
/// **'Select friends ({count} more needed)'**
String passwordlessRecoverySelectFriendsNeeded(num count);
/// No description provided for @passwordlessRecoverySelectFriends.
///
/// In en, this message translates to:
/// **'Select trusted friends'**
String get passwordlessRecoverySelectFriends;
/// No description provided for @passwordlessRecoveryNoFriendsSelected.
///
/// In en, this message translates to:
/// **'No trusted friends selected yet'**
String get passwordlessRecoveryNoFriendsSelected;
/// No description provided for @passwordlessRecoverySubtitle.
///
/// In en, this message translates to:
/// **'Recover your identity without a password.'**
String get passwordlessRecoverySubtitle;
/// No description provided for @recoverPasswordlessExplanation.
///
/// In en, this message translates to:
/// **'If enabled, you can recover your account by asking your selected friends to help you.'**
String get recoverPasswordlessExplanation;
/// No description provided for @recoverPasswordlessNotificationCardTitle.
///
/// In en, this message translates to:
/// **'Enable notifications'**
String get recoverPasswordlessNotificationCardTitle;
/// No description provided for @recoverPasswordlessNotificationCardSubtitle.
///
/// In en, this message translates to:
/// **'Get notified when a friend has helped you recover your account.'**
String get recoverPasswordlessNotificationCardSubtitle;
/// No description provided for @recoverPasswordlessNotificationCardBtn.
///
/// In en, this message translates to:
/// **'Enable push notifications'**
String get recoverPasswordlessNotificationCardBtn;
/// No description provided for @recoverPasswordlessQrInstructions.
///
/// In en, this message translates to:
/// **'Let friends scan the QR code or share the link.'**
String get recoverPasswordlessQrInstructions;
/// No description provided for @recoverPasswordlessShareBtn.
///
/// In en, this message translates to:
/// **'Share Recovery Link'**
String get recoverPasswordlessShareBtn;
/// No description provided for @recoverPasswordlessCopyBtn.
///
/// In en, this message translates to:
/// **'Copy'**
String get recoverPasswordlessCopyBtn;
/// No description provided for @recoverPasswordlessCopiedSnackbar.
///
/// In en, this message translates to:
/// **'Link copied to clipboard!'**
String get recoverPasswordlessCopiedSnackbar;
/// No description provided for @passwordlessRecoveryNoShareStored.
///
/// In en, this message translates to:
/// **'No recovery share stored for this contact.'**
String get passwordlessRecoveryNoShareStored;
/// No description provided for @passwordlessRecoveryShareSent.
///
/// In en, this message translates to:
/// **'Recovery share successfully sent!'**
String get passwordlessRecoveryShareSent;
/// No description provided for @passwordlessRecoveryNetworkError.
///
/// In en, this message translates to:
/// **'Network error, please ensure you have internet'**
String get passwordlessRecoveryNetworkError;
/// No description provided for @passwordlessRecoveryInvalidEmail.
///
/// In en, this message translates to:
/// **'The email address is invalid.'**
String get passwordlessRecoveryInvalidEmail;
/// No description provided for @passwordlessRecoveryResendEmail.
///
/// In en, this message translates to:
/// **'Resend recovery email'**
String get passwordlessRecoveryResendEmail;
/// No description provided for @passwordlessRecoveryHelpAFriend.
///
/// In en, this message translates to:
/// **'Help a Friend'**
String get passwordlessRecoveryHelpAFriend;
/// No description provided for @passwordlessRecoverySelectContactDesc.
///
/// In en, this message translates to:
/// **'Please select the contact who requested recovery below to send them their recovery key.'**
String get passwordlessRecoverySelectContactDesc;
/// No description provided for @passwordlessRecoverySearchContacts.
///
/// In en, this message translates to:
/// **'Search contacts...'**
String get passwordlessRecoverySearchContacts;
/// No description provided for @passwordlessRecoveryNoContactsFound.
///
/// In en, this message translates to:
/// **'No contacts found'**
String get passwordlessRecoveryNoContactsFound;
/// No description provided for @passwordlessRecoveryCantHelpHim.
///
/// In en, this message translates to:
/// **'You can\'t help him'**
String get passwordlessRecoveryCantHelpHim;
/// No description provided for @passwordlessRecoveryDoesAskedYou.
///
/// In en, this message translates to:
/// **'Does {username} has asked you?'**
String passwordlessRecoveryDoesAskedYou(Object username);
/// No description provided for @passwordlessRecoveryVerifySourceDesc.
///
/// In en, this message translates to:
/// **'Please ensure that you actualy received the link/qr code from your friend! If you are unsure, please verify again and then click again on the link or come back.'**
String get passwordlessRecoveryVerifySourceDesc;
/// No description provided for @passwordlessRecoveryNo.
///
/// In en, this message translates to:
/// **'No'**
String get passwordlessRecoveryNo;
/// No description provided for @passwordlessRecoveryYes.
///
/// In en, this message translates to:
/// **'Yes'**
String get passwordlessRecoveryYes;
/// No description provided for @passwordlessRecoveryYesWithTimer.
///
/// In en, this message translates to:
/// **'Yes ({seconds}s)'**
String passwordlessRecoveryYesWithTimer(Object seconds);
/// No description provided for @recoverPasswordlessBeingRecoveredLabel.
///
/// In en, this message translates to:
/// **'Account being recovered'**
String get recoverPasswordlessBeingRecoveredLabel;
/// No description provided for @recoverPasswordlessSharesReceived.
///
/// In en, this message translates to:
/// **'{received} of {threshold} friends have shared'**
String recoverPasswordlessSharesReceived(Object received, Object threshold);
/// No description provided for @recoverPasswordlessRecoverNowBtn.
///
/// In en, this message translates to:
/// **'Recover now'**
String get recoverPasswordlessRecoverNowBtn;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -33,22 +33,12 @@ class AppLocalizationsDe extends AppLocalizations {
String get onboardingFocusBody => String get onboardingFocusBody =>
'Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.'; 'Verabschiede dich von süchtig machenden Funktionen! twonly wurde für das Teilen von Momenten ohne nutzlose Ablenkungen oder Werbung entwickelt.';
@override
String get onboardingSendTwonliesTitle => 'twonlies senden';
@override
String get onboardingSendTwonliesBody =>
'Teile Momente sicher mit deinem Partner. twonly stellt sicher, dass nur dein Partner sie öffnen kann, sodass deine Momente mit deinem Partner eine two(o)nly Sache bleiben!';
@override @override
String get onboardingNotProductTitle => 'Du bist nicht das Produkt!'; String get onboardingNotProductTitle => 'Du bist nicht das Produkt!';
@override @override
String get onboardingNotProductBody => String get onboardingNotProductBody =>
'twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.'; 'twonly wird durch ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.';
@override
String get onboardingGetStartedTitle => 'Auf geht\'s';
@override @override
String get registerUsernameSlogan => 'Konto erstellen'; String get registerUsernameSlogan => 'Konto erstellen';
@ -126,18 +116,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get searchUsernameNotFound => 'Benutzername nicht gefunden'; String get searchUsernameNotFound => 'Benutzername nicht gefunden';
@override
String searchUsernameNotFoundBody(Object username) {
return 'Es wurde kein Benutzer mit dem Benutzernamen \"$username\" gefunden.';
}
@override @override
String get searchUsernameNewFollowerTitle => 'Offene Anfragen'; String get searchUsernameNewFollowerTitle => 'Offene Anfragen';
@override
String get chatListViewSearchUserNameBtn =>
'Füge deinen ersten twonly-Kontakt hinzu!';
@override @override
String get chatListDetailInput => 'Nachricht eingeben'; String get chatListDetailInput => 'Nachricht eingeben';
@ -229,10 +210,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern'; String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern';
@override
String get settingsStorageDataStoreInGSubtitle =>
'Speichere Bilder zusätzlich in der Systemgalerie.';
@override @override
String get settingsStorageDataMediaAutoDownload => String get settingsStorageDataMediaAutoDownload =>
'Automatischer Mediendownload'; 'Automatischer Mediendownload';
@ -280,45 +257,17 @@ class AppLocalizationsDe extends AppLocalizations {
String get settingsPrivacy => 'Datenschutz & Sicherheit'; String get settingsPrivacy => 'Datenschutz & Sicherheit';
@override @override
String get settingsPrivacyBlockUsers => 'Benutzer blockieren'; String get settingsPrivacyBlockUsers => 'Kontakte blockieren';
@override @override
String get settingsPrivacyBlockUsersDesc => String get settingsPrivacyBlockUsersDesc =>
'Blockierte Benutzer können nicht mit dir kommunizieren. Du kannst einen blockierten Benutzer jederzeit wieder entsperren.'; 'Blockierte Kontakte können nicht mit dir kommunizieren. Du kannst einen blockierten Kontakt jederzeit wieder entsperren.';
@override @override
String settingsPrivacyBlockUsersCount(Object len) { String settingsPrivacyBlockUsersCount(Object len) {
return '$len Kontakt(e)'; return '$len Kontakt(e)';
} }
@override
String get settingsPrivacyProfileSelectionTitle => 'Sicherheitsprofil';
@override
String get settingsPrivacyProfileSelectionDesc =>
'Wähle deinen Setup-Pfad und deine Sicherheitskonfiguration';
@override
String get securityProfileTitle => 'Sicherheitsprofil';
@override
String get securityProfileSubtitle =>
'Wähle das Schutzniveau, das zu deiner täglichen Nutzung passt. Dies kann jederzeit in den Einstellungen geändert werden.';
@override
String get securityProfileNormalTitle => 'Normaler Schutz';
@override
String get securityProfileNormalDesc =>
'Gute Balance zwischen Komfort und Sicherheit, ohne dich zu sehr einzuschränken.';
@override
String get securityProfileStrictTitle => 'Strikter Schutz';
@override
String get securityProfileStrictDesc =>
'Maximaler Schutz vor Phishing, kann aber unkomfortabel sein.';
@override @override
String get settingsNotification => 'Benachrichtigung'; String get settingsNotification => 'Benachrichtigung';
@ -431,7 +380,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contactUsLastWarning => String get contactUsLastWarning =>
'Dies sind die Informationen, die an uns gesendet werden. Bitte prüfen Sie sie und klicke dann auf „Abschicken“.'; 'Dies sind die Informationen, die an uns gesendet werden. Bitte prüfe sie und klicke dann auf „Abschicken“.';
@override @override
String get contactUsSuccess => 'Feedback erfolgreich übermittelt!'; String get contactUsSuccess => 'Feedback erfolgreich übermittelt!';
@ -460,16 +409,45 @@ class AppLocalizationsDe extends AppLocalizations {
'Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.'; 'Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.';
@override @override
String get contactVerifyNumberTitle => 'Benutzer verifizieren'; String get contactVerifyNumberTitle => 'Kontakte verifizieren';
@override @override
String get userVerifiedTitle => 'Benutzer verifiziert'; String verifyUserIdentity(Object username) {
return 'Identität von $username verifizieren';
}
@override
String get contactVerifyNumberSubtitle =>
'Überprüfe die Identität deiner Kontakte, um sicherzugehen, dass du mit der richtigen Person schreibst.';
@override
String get inChatContactNotVerified => 'Kontakt nicht verifiziert.';
@override
String groupMembersNotVerified(Object count) {
return '$count Mitglieder sind nicht verifiziert.';
}
@override
String get userVerifiedTitle => 'Kontakt verifiziert';
@override
String scanUserQrCode(Object username) {
return 'QR-Code von $username scannen';
}
@override
String get openOwnQrCode => 'Eigenen QR-Code öffnen';
@override @override
String contactVerifiedBy(Object username) { String contactVerifiedBy(Object username) {
return 'Verifiziert von $username'; return 'Verifiziert von $username';
} }
@override
String get contactSharedByUnknown =>
'Geteilt von einem verifizierten Kontakt (Benutzername nicht verfügbar)';
@override @override
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.'; String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
@ -508,10 +486,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contactBlockBody => String get contactBlockBody =>
'Ein blockierter Benutzer kann dir keine Nachrichten mehr senden, und deren Profil ist nicht mehr sichtbar. Um die Blockierung eines Benutzers aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Benutzer.'; 'Ein blockierter Kontakt kann dir keine Nachrichten mehr senden, und deren Profil ist nicht mehr sichtbar. Um die Blockierung eines Kontakts aufzuheben, navigiere einfach zu Einstellungen > Datenschutz > Blockierte Kontakte.';
@override @override
String get contactRemove => 'Benutzer löschen'; String get contactRemove => 'Kontakt löschen';
@override @override
String contactRemoveTitle(Object username) { String contactRemoveTitle(Object username) {
@ -520,7 +498,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contactRemoveBody => String get contactRemoveBody =>
'Den Benutzer dauerhaft entfernen. Wenn der Benutzer versucht, dir eine neue Nachricht zu senden, musst du den Benutzer erst wieder akzeptieren.'; 'Den Kontakt dauerhaft entfernen. Wenn der Kontakt versucht, dir eine neue Nachricht zu senden, musst du den Kontakt erst wieder akzeptieren.';
@override @override
String get undo => 'Rückgängig'; String get undo => 'Rückgängig';
@ -860,7 +838,14 @@ class AppLocalizationsDe extends AppLocalizations {
String get backupTwonlySaveNow => 'Jetzt speichern'; String get backupTwonlySaveNow => 'Jetzt speichern';
@override @override
String get backupChangePassword => 'Password ändern'; String get backupChangePassword => 'Passwort ändern';
@override
String get backupChangePasswordAuthReason => 'Backup-Passwort ändern';
@override
String get backupChangePasswordAuthFailed =>
'Du kannst dein Passwort nur ändern, wenn du dich authentifiziert hast!';
@override @override
String get twonlySafeRecoverTitle => 'Backup wiederherstellen'; String get twonlySafeRecoverTitle => 'Backup wiederherstellen';
@ -910,7 +895,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get reportUserReason => 'Meldegrund'; String get reportUserReason => 'Meldegrund';
@override @override
String get reportUser => 'Benutzer melden'; String get reportUser => 'Kontakt melden';
@override @override
String get newDeviceRegistered => String get newDeviceRegistered =>
@ -1045,6 +1030,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get alreadyInGroup => 'Bereits Mitglied'; String get alreadyInGroup => 'Bereits Mitglied';
@override
String get contactNotVerified => 'Nicht verifiziert';
@override @override
String removeContactFromGroupTitle(Object username) { String removeContactFromGroupTitle(Object username) {
return '$username aus dieser Gruppe entfernen?'; return '$username aus dieser Gruppe entfernen?';
@ -1307,12 +1295,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get draftMessage => 'Entwurf'; String get draftMessage => 'Entwurf';
@override
String get exportMemories => 'Memories exportieren (Beta)';
@override
String get importMemories => 'Memories importieren (Beta)';
@override @override
String get voiceMessageSlideToCancel => 'Zum Abbrechen ziehen'; String get voiceMessageSlideToCancel => 'Zum Abbrechen ziehen';
@ -1332,6 +1314,11 @@ class AppLocalizationsDe extends AppLocalizations {
String get addContactQrSheetSubtext => String get addContactQrSheetSubtext =>
'Lass einen Freund diesen QR-Code scannen, um dich hinzuzufügen'; 'Lass einen Freund diesen QR-Code scannen, um dich hinzuzufügen';
@override
String letUserScanQrCode(Object username) {
return 'Lass $username diesen QR-Code scannen';
}
@override @override
String get finishSetupCardTitle => 'Profil vervollständigen'; String get finishSetupCardTitle => 'Profil vervollständigen';
@ -1458,7 +1445,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String verifiedPublicKey(Object username) { String verifiedPublicKey(Object username) {
return 'Der öffentliche Schlüssel von $username wurde überprüft und ist gültig.'; return 'Die Identität von $username wurde erfolgreich überprüft.';
} }
@override @override
@ -1557,11 +1544,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get verificationBadgeGeneralDesc => String get verificationBadgeGeneralDesc =>
'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Du kannst Kontakte jederzeit verifizieren, indem du deren QR-Code scannst.'; 'Der Haken gibt dir die Sicherheit, dass du mit der *richtigen Person* schreibst und deine *Nachrichten vertraulich* bleiben.';
@override @override
String get verificationBadgeGreenDesc => String get verificationBadgeGreenDesc =>
'Ein Kontakt, den du *persönlich verifiziert* hast.'; 'Ein Kontakt, den du über den QR-code *persönlich verifiziert* hast.';
@override @override
String get verificationBadgeYellowDesc => String get verificationBadgeYellowDesc =>
@ -1571,6 +1558,25 @@ class AppLocalizationsDe extends AppLocalizations {
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.'; 'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
@override
String get scanNow => 'Jetzt scannen';
@override
String get qrScannerVerifyHint =>
'Zum Verifizieren muss die andere Person ihren QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)';
@override
String qrScannerVerifyUserHint(Object username) {
return 'Zum Verifizieren muss $username seinen QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)';
}
@override
String get openQrCode => 'QR-Code öffnen';
@override
String get letFriendScanQrToVerify =>
'Lass einen Freund diesen QR-Code scannen, um euch zu verifizieren';
@override @override
String get deleteVerificationTitle => 'Verifizierung löschen?'; String get deleteVerificationTitle => 'Verifizierung löschen?';
@ -1583,6 +1589,11 @@ class AppLocalizationsDe extends AppLocalizations {
return '$username hat deinen QR-Code gescannt und ist nun verifiziert.'; return '$username hat deinen QR-Code gescannt und ist nun verifiziert.';
} }
@override
String askForFriendPromotionsPrompt(Object username) {
return 'Hilf $username, bekannte Gesichter auf twonly zu finden, indem du gemeinsame Freunde teilst.';
}
@override @override
String mutualGroupsTitle(num count) { String mutualGroupsTitle(num count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@ -1594,17 +1605,6 @@ class AppLocalizationsDe extends AppLocalizations {
return '$_temp0'; return '$_temp0';
} }
@override
String mutualGroupsSentMessages(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count Nachrichten gesendet',
one: '1 Nachricht gesendet',
);
return '$_temp0';
}
@override @override
String chatEntryFlameRestored(Object count) { String chatEntryFlameRestored(Object count) {
return '$count Flammen wiederhergestellt'; return '$count Flammen wiederhergestellt';
@ -1650,9 +1650,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get settingsTypingIndicationSubtitle => String get settingsTypingIndicationSubtitle =>
'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.'; 'Bei deaktivierten Tipp-Indikatoren kannst du nicht sehen, wenn andere gerade eine Nachricht tippen.';
@override
String get scanQrOrShow => 'QR scannen / anzeigen';
@override @override
String get contactActionBlock => 'Blockieren'; String get contactActionBlock => 'Blockieren';
@ -1684,9 +1681,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get userDiscoverySettingsTitle => 'Gemeinsame Freunde'; String get userDiscoverySettingsTitle => 'Gemeinsame Freunde';
@override
String get userDiscoveryWhyThisIsUsed => 'Warum dies verwendet wird';
@override @override
String get userDiscoveryFeatureOffers => 'Dein Nutzen auf einen Blick'; String get userDiscoveryFeatureOffers => 'Dein Nutzen auf einen Blick';
@ -1854,9 +1848,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get back => 'Zurück'; String get back => 'Zurück';
@override
String get onboardingExampleLabel => 'Beispiel';
@override @override
String makerChangedUsername(Object maker, Object oldName, Object newName) { String makerChangedUsername(Object maker, Object oldName, Object newName) {
return '$maker hat den Benutzernamen von $oldName zu $newName geändert.'; return '$maker hat den Benutzernamen von $oldName zu $newName geändert.';
@ -1957,9 +1948,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get dragToZoom => 'Zum Zoomen ziehen'; String get dragToZoom => 'Zum Zoomen ziehen';
@override
String get showUsername => 'Benutzernamen anzeigen';
@override @override
String get onboardingProfileSelectionTitle => 'Wähle deinen Setup-Weg'; String get onboardingProfileSelectionTitle => 'Wähle deinen Setup-Weg';
@ -2094,20 +2082,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get importGalleryNoImagesFoundDesc => String get importGalleryNoImagesFoundDesc =>
'Es befinden sich keine Bilder auf deinem Gerät.'; 'Es befinden sich keine Bilder auf deinem Gerät.';
@override
String get importGalleryShowAllImages => 'Alle Bilder anzeigen';
@override
String get importGalleryShowTwonlyAlbum => 'twonly-Album anzeigen';
@override
String get importGalleryToggleDescAll =>
'Es werden alle Bilder auf deinem Gerät angezeigt.';
@override
String get importGalleryToggleDescTwonly =>
'Es wird das \"twonly\"-Album angezeigt.';
@override @override
String get importGalleryFilterTwonly => 'Nur das twonly-Album anzeigen'; String get importGalleryFilterTwonly => 'Nur das twonly-Album anzeigen';
@ -2150,4 +2124,274 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get avatarCustomizeReset => 'Zurücksetzen'; String get avatarCustomizeReset => 'Zurücksetzen';
@override
String get passwordlessRecovery => 'Passwortloses Backup';
@override
String get passwordlessRecoveryNotConfigured => 'Nicht konfiguriert';
@override
String get passwordlessRecoveryTestPin => 'PIN testen';
@override
String get passwordlessRecoveryTestPinTitle => 'PIN testen';
@override
String get passwordlessRecoveryTestPinHint => 'Gib deine PIN ein';
@override
String get passwordlessRecoveryTestPinCorrect => 'PIN ist korrekt!';
@override
String get passwordlessRecoveryTestPinIncorrect => 'Falsche PIN.';
@override
String get passwordlessRecoveryTest => 'Testen';
@override
String get passwordlessRecoverySecondFactorNone => 'Keiner';
@override
String passwordlessRecoverySecondFactorEmailLabel(Object email) {
return 'E-Mail ($email)';
}
@override
String get passwordlessRecoverySecondFactorPin => 'PIN';
@override
String get passwordlessRecoverySecondFactorEmail => 'E-Mail';
@override
String get passwordlessRecoveryModify => 'Einstellungen bearbeiten';
@override
String get passwordlessRecoveryModifyDesc =>
'Aktualisiere deine Backupkonfiguration';
@override
String get passwordlessRecoverySecondFactor => 'Zweiter Faktor';
@override
String get passwordlessRecoveryNoFriendsFound => 'Keine Freunde gefunden.';
@override
String get passwordlessRecoveryActiveFriends => 'Aktive Freunde';
@override
String get passwordlessRecoveryActiveFriendsDesc =>
'Diese Freunde nutzen twonly aktiv und können dir wahrscheinlich helfen, dein Konto wiederherzustellen.';
@override
String get passwordlessRecoveryInactiveFriends => 'Inaktive Freunde';
@override
String get passwordlessRecoveryInactiveFriendsDesc =>
'Diese Freunde haben entweder ihren Teil noch nicht erhalten oder nutzen twonly nicht mehr aktiv, was bedeutet, dass sie möglicherweise nicht helfen können.';
@override
String get passwordlessRecoveryNotEnoughFriends =>
'Nicht genügend Freunde ausgewählt. Du brauchst mindestens 3.';
@override
String get passwordlessRecoveryLoading => 'Wird geladen...';
@override
String get passwordlessRecoveryEnableSuccess =>
'Passwortloses Backup erfolgreich aktiviert!';
@override
String get passwordlessRecoveryEnableFailed =>
'Fehler beim Aktivieren des passwortlosen Backups.';
@override
String get passwordlessRecoveryNeedAtLeast3 =>
'Du brauchst mindestens 3 Freunde.';
@override
String get passwordlessRecoveryInvalidPin => 'Ungültige PIN';
@override
String get passwordlessRecoveryEnterPin => 'Bitte gib eine PIN ein.';
@override
String get passwordlessRecoveryPinMinLength =>
'Die PIN muss mindestens 4 Ziffern lang sein.';
@override
String get passwordlessRecoveryEnterEmail =>
'Bitte gib eine E-Mail-Adresse ein.';
@override
String get passwordlessRecoveryEnableBtn => 'Passwortloses Backup aktivieren';
@override
String get passwordlessRecoveryRecoverBtn => 'Passwortlos wiederherstellen';
@override
String get passwordlessRecoveryModifyBtn => 'Passwortloses Backup bearbeiten';
@override
String passwordlessRecoveryStatusEnabled(num count) {
return 'Aktiviert • $count Freunde';
}
@override
String get passwordlessRecoveryInfoHowItWorks => 'Wie es funktioniert';
@override
String get passwordlessRecoveryInfoHowItWorksDesc =>
'Da twonly ohne zentrale Benutzerkonten oder Telefonnummern funktioniert, um deine Privatsphäre zu maximieren, setzen wir auf einen dezentralen Wiederherstellungsmechanismus. Mit Shamir\'s Secret Sharing wird deine kryptografische Identität in unabhängige Teile aufgeteilt und an deine Freunde verteilt. Um den Zugriff wiederherzustellen, muss ein vorher festgelegter Schwellenwert dieser Freunde ihre Teile kombinieren.';
@override
String get passwordlessRecoveryInfoWhySecondFactor =>
'Warum ein zweiter Faktor?';
@override
String get passwordlessRecoveryInfoWhySecondFactorDesc =>
'Der zweite Faktor (E-Mail oder PIN) dient als wichtiger kryptografischer Schutz vor bösartiger Absprache. Wenn sich deine Freunde hinter deinem Rücken absprechen würden, könnten sie deine Identität ohne den Schlüssel für den zweiten Faktor trotzdem nicht entschlüsseln.';
@override
String get passwordlessRecoveryInfoGotIt => 'Verstanden';
@override
String get passwordlessRecoveryMethod => 'Methode für den zweiten Faktor';
@override
String get passwordlessRecoveryMethodNoneDesc =>
'Ohne zweiten Faktor könnten sich deine Freunde absprechen, um dein Konto wiederherzustellen. Daher wird empfohlen, einen zweiten Faktor zu konfigurieren.';
@override
String get passwordlessRecoveryMethodPinHint => 'PIN eingeben';
@override
String get passwordlessRecoveryMethodEmailHint =>
'Wiederherstellungs-E-Mail-Adresse eingeben';
@override
String get passwordlessRecoveryMethodEmailDesc =>
'Deine E-Mail-Adresse wird *niemals auf dem Server gespeichert* und nur im Falle einer Wiederherstellung an ihn gesendet.';
@override
String passwordlessRecoveryThresholdDesc(num count) {
return 'Um dein Konto wiederherzustellen, brauchst du $count deiner ausgewählten Freunde.';
}
@override
String get passwordlessRecoveryThresholdTitle =>
'Benötigte Freunde für das Backup';
@override
String passwordlessRecoverySelectFriendsNeeded(num count) {
return 'Freunde auswählen ($count weitere benötigt)';
}
@override
String get passwordlessRecoverySelectFriends => 'Freunde auswählen';
@override
String get passwordlessRecoveryNoFriendsSelected =>
'Noch keine Freunde ausgewählt';
@override
String get passwordlessRecoverySubtitle =>
'Stelle deine Identität ohne Passwort wieder her.';
@override
String get recoverPasswordlessExplanation =>
'Wenn du die passwortlose Wiederherstellung aktiviert hast, kannst du dein Konto mithilfe deiner Freunde wiederherstellen.';
@override
String get recoverPasswordlessNotificationCardTitle =>
'Benachrichtigungen aktivieren';
@override
String get recoverPasswordlessNotificationCardSubtitle =>
'Lass dich benachrichtigen, wenn ein Freund dir geholfen hat, dein Konto wiederherzustellen.';
@override
String get recoverPasswordlessNotificationCardBtn =>
'Push-Benachrichtigungen aktivieren';
@override
String get recoverPasswordlessQrInstructions =>
'Lass deine Freunde den QR-Code scannen oder teile den Link.';
@override
String get recoverPasswordlessShareBtn => 'Wiederherstellungs-Link teilen';
@override
String get recoverPasswordlessCopyBtn => 'Kopieren';
@override
String get recoverPasswordlessCopiedSnackbar =>
'Link in die Zwischenablage kopiert!';
@override
String get passwordlessRecoveryNoShareStored =>
'Kein Wiederherstellungs-Teil für diesen Kontakt gespeichert.';
@override
String get passwordlessRecoveryShareSent =>
'Wiederherstellungs-Teil erfolgreich gesendet!';
@override
String get passwordlessRecoveryNetworkError =>
'Netzwerkfehler, bitte stelle sicher, dass du Internet hast';
@override
String get passwordlessRecoveryInvalidEmail =>
'Die E-Mail-Adresse ist ungültig.';
@override
String get passwordlessRecoveryResendEmail => 'E-Mail erneut senden';
@override
String get passwordlessRecoveryHelpAFriend => 'Einem Freund helfen';
@override
String get passwordlessRecoverySelectContactDesc =>
'Wähle den Kontakt aus, dem du helfen möchtest.';
@override
String get passwordlessRecoverySearchContacts => 'Kontakte suchen...';
@override
String get passwordlessRecoveryNoContactsFound => 'Keine Kontakte gefunden';
@override
String get passwordlessRecoveryCantHelpHim => 'Du kannst ihm nicht helfen';
@override
String passwordlessRecoveryDoesAskedYou(Object username) {
return 'Hat $username dich gefragt?';
}
@override
String get passwordlessRecoveryVerifySourceDesc =>
'Bitte stelle sicher, dass du den Link/QR-Code tatsächlich von deinem Freund erhalten hast! Wenn du dir unsicher bist, verifiziere dies bitte erneut und klicke dann noch einmal auf den Link oder kehre zurück.';
@override
String get passwordlessRecoveryNo => 'Nein';
@override
String get passwordlessRecoveryYes => 'Ja';
@override
String passwordlessRecoveryYesWithTimer(Object seconds) {
return 'Ja (${seconds}s)';
}
@override
String get recoverPasswordlessBeingRecoveredLabel =>
'Konto wird wiederhergestellt';
@override
String recoverPasswordlessSharesReceived(Object received, Object threshold) {
return '$received von $threshold Freunden haben geteilt';
}
@override
String get recoverPasswordlessRecoverNowBtn => 'Jetzt wiederherstellen';
} }

View file

@ -32,22 +32,12 @@ class AppLocalizationsEn extends AppLocalizations {
String get onboardingFocusBody => String get onboardingFocusBody =>
'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.'; 'Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.';
@override
String get onboardingSendTwonliesTitle => 'Send twonlies';
@override
String get onboardingSendTwonliesBody =>
'Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!';
@override @override
String get onboardingNotProductTitle => 'You are not the product!'; String get onboardingNotProductTitle => 'You are not the product!';
@override @override
String get onboardingNotProductBody => String get onboardingNotProductBody =>
'twonly is financed by donations and an optional subscription. Your data will never be sold.'; 'twonly is financed by an optional subscription. Your data will never be sold.';
@override
String get onboardingGetStartedTitle => 'Let\'s go!';
@override @override
String get registerUsernameSlogan => 'Create your account'; String get registerUsernameSlogan => 'Create your account';
@ -125,17 +115,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get searchUsernameNotFound => 'Username not found'; String get searchUsernameNotFound => 'Username not found';
@override
String searchUsernameNotFoundBody(Object username) {
return 'There is no user with the username \"$username\" registered';
}
@override @override
String get searchUsernameNewFollowerTitle => 'Open requests'; String get searchUsernameNewFollowerTitle => 'Open requests';
@override
String get chatListViewSearchUserNameBtn => 'Add your first twonly contact!';
@override @override
String get chatListDetailInput => 'Type a message'; String get chatListDetailInput => 'Type a message';
@ -226,10 +208,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsStorageDataStoreInGTitle => 'Store in Gallery'; String get settingsStorageDataStoreInGTitle => 'Store in Gallery';
@override
String get settingsStorageDataStoreInGSubtitle =>
'Store saved images additional in the systems gallery.';
@override @override
String get settingsStorageDataMediaAutoDownload => 'Media auto-download'; String get settingsStorageDataMediaAutoDownload => 'Media auto-download';
@ -276,45 +254,17 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsPrivacy => 'Privacy & Security'; String get settingsPrivacy => 'Privacy & Security';
@override @override
String get settingsPrivacyBlockUsers => 'Block users'; String get settingsPrivacyBlockUsers => 'Block contacts';
@override @override
String get settingsPrivacyBlockUsersDesc => String get settingsPrivacyBlockUsersDesc =>
'Blocked users will not be able to communicate with you. You can unblock a blocked user at any time.'; 'Blocked contacts will not be able to communicate with you. You can unblock a blocked contact at any time.';
@override @override
String settingsPrivacyBlockUsersCount(Object len) { String settingsPrivacyBlockUsersCount(Object len) {
return '$len contact(s)'; return '$len contact(s)';
} }
@override
String get settingsPrivacyProfileSelectionTitle => 'Security Profile';
@override
String get settingsPrivacyProfileSelectionDesc =>
'Choose your setup path and security configuration';
@override
String get securityProfileTitle => 'Security Profile';
@override
String get securityProfileSubtitle =>
'Choose the level of protection that fits your daily use. This can be changed at any time in your settings.';
@override
String get securityProfileNormalTitle => 'Normal Protection';
@override
String get securityProfileNormalDesc =>
'Good balance between a convenient mode without bothering you too much.';
@override
String get securityProfileStrictTitle => 'Strict Protection';
@override
String get securityProfileStrictDesc =>
'Maximum anti-phishing protection but may be inconvenient.';
@override @override
String get settingsNotification => 'Notification'; String get settingsNotification => 'Notification';
@ -455,16 +405,45 @@ class AppLocalizationsEn extends AppLocalizations {
'Your account will be deleted. There is no change to restore it.'; 'Your account will be deleted. There is no change to restore it.';
@override @override
String get contactVerifyNumberTitle => 'Verify contact'; String get contactVerifyNumberTitle => 'Verify contacts';
@override @override
String get userVerifiedTitle => 'User verified'; String verifyUserIdentity(Object username) {
return 'Verify $username\'s identity';
}
@override
String get contactVerifyNumberSubtitle =>
'Verify the identity of your contacts to make sure you are texting the right person.';
@override
String get inChatContactNotVerified => 'Contact not verified.';
@override
String groupMembersNotVerified(Object count) {
return '$count members are not verified.';
}
@override
String get userVerifiedTitle => 'Contact verified';
@override
String scanUserQrCode(Object username) {
return 'Scan $username\'s QR code';
}
@override
String get openOwnQrCode => 'Open your own QR code';
@override @override
String contactVerifiedBy(Object username) { String contactVerifiedBy(Object username) {
return 'Verified by $username'; return 'Verified by $username';
} }
@override
String get contactSharedByUnknown =>
'Shared by a verified contact (username not available)';
@override @override
String get verificationTypeQrScanned => 'You scanned their QR code.'; String get verificationTypeQrScanned => 'You scanned their QR code.';
@ -503,10 +482,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contactBlockBody => String get contactBlockBody =>
'A blocked user will no longer be able to send you messages and their profile will be hidden from view. To unblock a user, simply navigate to Settings > Privacy > Blocked Users.'; 'A blocked contact will no longer be able to send you messages and their profile will be hidden from view. To unblock a contact, simply navigate to Settings > Privacy > Blocked Contacts.';
@override @override
String get contactRemove => 'Remove user'; String get contactRemove => 'Remove contact';
@override @override
String contactRemoveTitle(Object username) { String contactRemoveTitle(Object username) {
@ -515,7 +494,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get contactRemoveBody => String get contactRemoveBody =>
'Permanently remove the user. If the user tries to send you a new message, you will have to accept the user again first.'; 'Permanently remove the contact. If the contact tries to send you a new message, you will have to accept the contact again first.';
@override @override
String get undo => 'Undo'; String get undo => 'Undo';
@ -856,6 +835,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get backupChangePassword => 'Change password'; String get backupChangePassword => 'Change password';
@override
String get backupChangePasswordAuthReason => 'Changing backup password';
@override
String get backupChangePasswordAuthFailed =>
'You can only change your password after you have authenticated!';
@override @override
String get twonlySafeRecoverTitle => 'Restore backup'; String get twonlySafeRecoverTitle => 'Restore backup';
@ -904,7 +890,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get reportUserReason => 'Reporting reason'; String get reportUserReason => 'Reporting reason';
@override @override
String get reportUser => 'Report user'; String get reportUser => 'Report contact';
@override @override
String get newDeviceRegistered => String get newDeviceRegistered =>
@ -1038,6 +1024,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get alreadyInGroup => 'Already in Group'; String get alreadyInGroup => 'Already in Group';
@override
String get contactNotVerified => 'Not verified';
@override @override
String removeContactFromGroupTitle(Object username) { String removeContactFromGroupTitle(Object username) {
return 'Remove $username from this group?'; return 'Remove $username from this group?';
@ -1298,12 +1287,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get draftMessage => 'Draft'; String get draftMessage => 'Draft';
@override
String get exportMemories => 'Export memories (Beta)';
@override
String get importMemories => 'Import memories (Beta)';
@override @override
String get voiceMessageSlideToCancel => 'Slide to cancel'; String get voiceMessageSlideToCancel => 'Slide to cancel';
@ -1323,6 +1306,11 @@ class AppLocalizationsEn extends AppLocalizations {
String get addContactQrSheetSubtext => String get addContactQrSheetSubtext =>
'Let a friend scan this QR code to add you'; 'Let a friend scan this QR code to add you';
@override
String letUserScanQrCode(Object username) {
return 'Let $username scan this QR code';
}
@override @override
String get finishSetupCardTitle => 'Complete your profile'; String get finishSetupCardTitle => 'Complete your profile';
@ -1446,7 +1434,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String verifiedPublicKey(Object username) { String verifiedPublicKey(Object username) {
return 'The public key of $username has been verified and is valid.'; return 'The identity of $username has been successfully verified.';
} }
@override @override
@ -1543,11 +1531,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get verificationBadgeGeneralDesc => String get verificationBadgeGeneralDesc =>
'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.'; 'The badge gives you the peace of mind that you are messaging the *right person* and your *messages remain confidential*.';
@override @override
String get verificationBadgeGreenDesc => String get verificationBadgeGreenDesc =>
'A contact you have *personally* verified.'; 'A contact you have *personally verified* using the QR code.';
@override @override
String get verificationBadgeYellowDesc => String get verificationBadgeYellowDesc =>
@ -1557,6 +1545,25 @@ class AppLocalizationsEn extends AppLocalizations {
String get verificationBadgeRedDesc => String get verificationBadgeRedDesc =>
'A contact whose identity has *not* yet been verified.'; 'A contact whose identity has *not* yet been verified.';
@override
String get scanNow => 'Scan now';
@override
String get qrScannerVerifyHint =>
'To verify, the other person must open their QR code (Chat Lists > QR code button at the bottom right)';
@override
String qrScannerVerifyUserHint(Object username) {
return 'To verify, $username must open their QR code (Chat Lists > QR code button at the bottom right)';
}
@override
String get openQrCode => 'Open QR code';
@override
String get letFriendScanQrToVerify =>
'Let a friend scan this QR code to verify you';
@override @override
String get deleteVerificationTitle => 'Delete verification?'; String get deleteVerificationTitle => 'Delete verification?';
@ -1569,6 +1576,11 @@ class AppLocalizationsEn extends AppLocalizations {
return '$username has scanned your QR code and is now verified.'; return '$username has scanned your QR code and is now verified.';
} }
@override
String askForFriendPromotionsPrompt(Object username) {
return 'Help $username find familiar faces on twonly by sharing mutual friends.';
}
@override @override
String mutualGroupsTitle(num count) { String mutualGroupsTitle(num count) {
String _temp0 = intl.Intl.pluralLogic( String _temp0 = intl.Intl.pluralLogic(
@ -1580,17 +1592,6 @@ class AppLocalizationsEn extends AppLocalizations {
return '$_temp0'; return '$_temp0';
} }
@override
String mutualGroupsSentMessages(num count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count messages sent',
one: '1 message sent',
);
return '$_temp0';
}
@override @override
String chatEntryFlameRestored(Object count) { String chatEntryFlameRestored(Object count) {
return '$count flames restored'; return '$count flames restored';
@ -1636,9 +1637,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsTypingIndicationSubtitle => String get settingsTypingIndicationSubtitle =>
'When the typing indicator is turned off, you can\'t see when others are typing a message.'; 'When the typing indicator is turned off, you can\'t see when others are typing a message.';
@override
String get scanQrOrShow => 'Scan / Show QR';
@override @override
String get contactActionBlock => 'Block'; String get contactActionBlock => 'Block';
@ -1670,9 +1668,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get userDiscoverySettingsTitle => 'Mutual Friends'; String get userDiscoverySettingsTitle => 'Mutual Friends';
@override
String get userDiscoveryWhyThisIsUsed => 'Why this is used';
@override @override
String get userDiscoveryFeatureOffers => 'Your benefits at a glance'; String get userDiscoveryFeatureOffers => 'Your benefits at a glance';
@ -1840,9 +1835,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get back => 'Back'; String get back => 'Back';
@override
String get onboardingExampleLabel => 'Example';
@override @override
String makerChangedUsername(Object maker, Object oldName, Object newName) { String makerChangedUsername(Object maker, Object oldName, Object newName) {
return '$maker changed their username from $oldName to $newName.'; return '$maker changed their username from $oldName to $newName.';
@ -1942,9 +1934,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get dragToZoom => 'Drag to Zoom'; String get dragToZoom => 'Drag to Zoom';
@override
String get showUsername => 'Show username';
@override @override
String get onboardingProfileSelectionTitle => 'Choose your setup path'; String get onboardingProfileSelectionTitle => 'Choose your setup path';
@ -2079,18 +2068,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get importGalleryNoImagesFoundDesc => String get importGalleryNoImagesFoundDesc =>
'There are no images on your device.'; 'There are no images on your device.';
@override
String get importGalleryShowAllImages => 'Show all images';
@override
String get importGalleryShowTwonlyAlbum => 'Show twonly album';
@override
String get importGalleryToggleDescAll => 'Viewing all images on your device.';
@override
String get importGalleryToggleDescTwonly => 'Viewing the \"twonly\" album.';
@override @override
String get importGalleryFilterTwonly => 'Only show the twonly-Album'; String get importGalleryFilterTwonly => 'Only show the twonly-Album';
@ -2132,4 +2109,270 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get avatarCustomizeReset => 'Reset'; String get avatarCustomizeReset => 'Reset';
@override
String get passwordlessRecovery => 'Passwordless Recovery';
@override
String get passwordlessRecoveryNotConfigured => 'Not configured';
@override
String get passwordlessRecoveryTestPin => 'Test PIN';
@override
String get passwordlessRecoveryTestPinTitle => 'Test PIN';
@override
String get passwordlessRecoveryTestPinHint => 'Enter your PIN';
@override
String get passwordlessRecoveryTestPinCorrect => 'PIN is correct!';
@override
String get passwordlessRecoveryTestPinIncorrect => 'Incorrect PIN.';
@override
String get passwordlessRecoveryTest => 'Test';
@override
String get passwordlessRecoverySecondFactorNone => 'None';
@override
String passwordlessRecoverySecondFactorEmailLabel(Object email) {
return 'Email ($email)';
}
@override
String get passwordlessRecoverySecondFactorPin => 'PIN';
@override
String get passwordlessRecoverySecondFactorEmail => 'Email';
@override
String get passwordlessRecoveryModify => 'Modify Recovery Settings';
@override
String get passwordlessRecoveryModifyDesc =>
'Update your recovery configuration';
@override
String get passwordlessRecoverySecondFactor => 'Second Factor';
@override
String get passwordlessRecoveryNoFriendsFound => 'No trusted friends found.';
@override
String get passwordlessRecoveryActiveFriends => 'Active Friends';
@override
String get passwordlessRecoveryActiveFriendsDesc =>
'These trusted friends are actively using twonly and can probably help you recover your account.';
@override
String get passwordlessRecoveryInactiveFriends => 'Inactive Friends';
@override
String get passwordlessRecoveryInactiveFriendsDesc =>
'These friends either have not yet received their share or do not use twonly actively anymore, which could mean that they maybe cannot help.';
@override
String get passwordlessRecoveryNotEnoughFriends =>
'Not enough friends selected. You need at least 3.';
@override
String get passwordlessRecoveryLoading => 'Loading...';
@override
String get passwordlessRecoveryEnableSuccess =>
'Passwordless recovery successfully enabled!';
@override
String get passwordlessRecoveryEnableFailed =>
'Failed to enable passwordless recovery.';
@override
String get passwordlessRecoveryNeedAtLeast3 =>
'You need at least 3 trusted friends.';
@override
String get passwordlessRecoveryInvalidPin => 'Invalid PIN';
@override
String get passwordlessRecoveryEnterPin => 'Please enter a PIN.';
@override
String get passwordlessRecoveryPinMinLength =>
'PIN must be at least 4 digits.';
@override
String get passwordlessRecoveryEnterEmail => 'Please enter an email address.';
@override
String get passwordlessRecoveryEnableBtn => 'Enable Passwordless Recovery';
@override
String get passwordlessRecoveryRecoverBtn => 'Recover passwordless';
@override
String get passwordlessRecoveryModifyBtn => 'Modify Passwordless Recovery';
@override
String passwordlessRecoveryStatusEnabled(num count) {
return 'Enabled • $count trusted friends';
}
@override
String get passwordlessRecoveryInfoHowItWorks => 'How it works';
@override
String get passwordlessRecoveryInfoHowItWorksDesc =>
'Because twonly operates without central user accounts or phone numbers to maximize privacy, we rely on a decentralized recovery mechanism. Using Shamir\'s Secret Sharing, your cryptographic identity is split into independent shares and distributed among your trusted friends. To restore access, a predefined threshold of these friends must combine their shares.';
@override
String get passwordlessRecoveryInfoWhySecondFactor => 'Why a Second Factor?';
@override
String get passwordlessRecoveryInfoWhySecondFactorDesc =>
'The second factor (Email or PIN) serves as a vital cryptographic safeguard against malicious collusion. If your trusted friends were to coordinate their shares behind your back, they still wouldn\'t be able to decrypt your identity without the second factor key.';
@override
String get passwordlessRecoveryInfoGotIt => 'Got it';
@override
String get passwordlessRecoveryMethod => 'Second factor method';
@override
String get passwordlessRecoveryMethodNoneDesc =>
'Without second-factor, your friends could collaborate to recover your account. Therefore, it is recommended to configure a second-factor.';
@override
String get passwordlessRecoveryMethodPinHint => 'Enter PIN';
@override
String get passwordlessRecoveryMethodEmailHint =>
'Enter recovery email address';
@override
String get passwordlessRecoveryMethodEmailDesc =>
'Your email address is *never stored on the server* and is only sent to it in the event of a recovery.';
@override
String passwordlessRecoveryThresholdDesc(num count) {
return 'To recover your account you need $count of your selected trusted friends.';
}
@override
String get passwordlessRecoveryThresholdTitle =>
'Required trusted friends for recovery';
@override
String passwordlessRecoverySelectFriendsNeeded(num count) {
return 'Select friends ($count more needed)';
}
@override
String get passwordlessRecoverySelectFriends => 'Select trusted friends';
@override
String get passwordlessRecoveryNoFriendsSelected =>
'No trusted friends selected yet';
@override
String get passwordlessRecoverySubtitle =>
'Recover your identity without a password.';
@override
String get recoverPasswordlessExplanation =>
'If enabled, you can recover your account by asking your selected friends to help you.';
@override
String get recoverPasswordlessNotificationCardTitle => 'Enable notifications';
@override
String get recoverPasswordlessNotificationCardSubtitle =>
'Get notified when a friend has helped you recover your account.';
@override
String get recoverPasswordlessNotificationCardBtn =>
'Enable push notifications';
@override
String get recoverPasswordlessQrInstructions =>
'Let friends scan the QR code or share the link.';
@override
String get recoverPasswordlessShareBtn => 'Share Recovery Link';
@override
String get recoverPasswordlessCopyBtn => 'Copy';
@override
String get recoverPasswordlessCopiedSnackbar => 'Link copied to clipboard!';
@override
String get passwordlessRecoveryNoShareStored =>
'No recovery share stored for this contact.';
@override
String get passwordlessRecoveryShareSent =>
'Recovery share successfully sent!';
@override
String get passwordlessRecoveryNetworkError =>
'Network error, please ensure you have internet';
@override
String get passwordlessRecoveryInvalidEmail =>
'The email address is invalid.';
@override
String get passwordlessRecoveryResendEmail => 'Resend recovery email';
@override
String get passwordlessRecoveryHelpAFriend => 'Help a Friend';
@override
String get passwordlessRecoverySelectContactDesc =>
'Please select the contact who requested recovery below to send them their recovery key.';
@override
String get passwordlessRecoverySearchContacts => 'Search contacts...';
@override
String get passwordlessRecoveryNoContactsFound => 'No contacts found';
@override
String get passwordlessRecoveryCantHelpHim => 'You can\'t help him';
@override
String passwordlessRecoveryDoesAskedYou(Object username) {
return 'Does $username has asked you?';
}
@override
String get passwordlessRecoveryVerifySourceDesc =>
'Please ensure that you actualy received the link/qr code from your friend! If you are unsure, please verify again and then click again on the link or come back.';
@override
String get passwordlessRecoveryNo => 'No';
@override
String get passwordlessRecoveryYes => 'Yes';
@override
String passwordlessRecoveryYesWithTimer(Object seconds) {
return 'Yes (${seconds}s)';
}
@override
String get recoverPasswordlessBeingRecoveredLabel =>
'Account being recovered';
@override
String recoverPasswordlessSharesReceived(Object received, Object threshold) {
return '$received of $threshold friends have shared';
}
@override
String get recoverPasswordlessRecoverNowBtn => 'Recover now';
} }

@ -1 +1 @@
Subproject commit c95e98ca929d630ead028d84e13934b30dbeba3b Subproject commit b7d703c82a5ac0b6776847cb4eb6d637620f6585

View file

@ -0,0 +1,65 @@
import 'package:json_annotation/json_annotation.dart';
part 'onboarding_state.model.g.dart';
/// Holds information about a single trusted friend who has already shared their
/// recovery part.
@JsonSerializable()
class ReceivedRecoveryShare {
ReceivedRecoveryShare({
required this.messageId,
required this.trustedFriendDisplayName,
required this.myDisplayName,
required this.myUserId,
required this.myAvatarSvg,
required this.threshold,
required this.sharedSecretDataBytes,
});
factory ReceivedRecoveryShare.fromJson(Map<String, dynamic> json) =>
_$ReceivedRecoveryShareFromJson(json);
final int messageId;
final String trustedFriendDisplayName;
final String myDisplayName;
final int myUserId;
final List<int>? myAvatarSvg;
final int threshold;
final List<int> sharedSecretDataBytes;
Map<String, dynamic> toJson() => _$ReceivedRecoveryShareToJson(this);
}
@JsonSerializable()
class OnboardingState {
OnboardingState({
this.hasOnboardingFinished = false,
this.hasStartedPasswordlessRecovery = false,
this.notificationId,
this.downloadAuthToken,
this.serverRegistered = false,
this.encryptionKey,
this.emailRecoveryRequested = false,
});
factory OnboardingState.fromJson(Map<String, dynamic> json) =>
_$OnboardingStateFromJson(json);
bool hasOnboardingFinished;
bool hasStartedPasswordlessRecovery;
String? notificationId;
List<int>? downloadAuthToken;
bool serverRegistered;
List<int>? encryptionKey;
bool emailRecoveryRequested;
List<ReceivedRecoveryShare> receivedShares = [];
Map<String, dynamic> toJson() => _$OnboardingStateToJson(this);
}

View file

@ -0,0 +1,67 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'onboarding_state.model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson(
Map<String, dynamic> json,
) => ReceivedRecoveryShare(
messageId: (json['messageId'] as num).toInt(),
trustedFriendDisplayName: json['trustedFriendDisplayName'] as String,
myDisplayName: json['myDisplayName'] as String,
myUserId: (json['myUserId'] as num).toInt(),
myAvatarSvg: (json['myAvatarSvg'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
threshold: (json['threshold'] as num).toInt(),
sharedSecretDataBytes: (json['sharedSecretDataBytes'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$ReceivedRecoveryShareToJson(
ReceivedRecoveryShare instance,
) => <String, dynamic>{
'messageId': instance.messageId,
'trustedFriendDisplayName': instance.trustedFriendDisplayName,
'myDisplayName': instance.myDisplayName,
'myUserId': instance.myUserId,
'myAvatarSvg': instance.myAvatarSvg,
'threshold': instance.threshold,
'sharedSecretDataBytes': instance.sharedSecretDataBytes,
};
OnboardingState _$OnboardingStateFromJson(Map<String, dynamic> json) =>
OnboardingState(
hasOnboardingFinished: json['hasOnboardingFinished'] as bool? ?? false,
hasStartedPasswordlessRecovery:
json['hasStartedPasswordlessRecovery'] as bool? ?? false,
notificationId: json['notificationId'] as String?,
downloadAuthToken: (json['downloadAuthToken'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
serverRegistered: json['serverRegistered'] as bool? ?? false,
encryptionKey: (json['encryptionKey'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
emailRecoveryRequested:
json['emailRecoveryRequested'] as bool? ?? false,
)
..receivedShares = (json['receivedShares'] as List<dynamic>)
.map((e) => ReceivedRecoveryShare.fromJson(e as Map<String, dynamic>))
.toList();
Map<String, dynamic> _$OnboardingStateToJson(OnboardingState instance) =>
<String, dynamic>{
'hasOnboardingFinished': instance.hasOnboardingFinished,
'hasStartedPasswordlessRecovery': instance.hasStartedPasswordlessRecovery,
'notificationId': instance.notificationId,
'downloadAuthToken': instance.downloadAuthToken,
'serverRegistered': instance.serverRegistered,
'encryptionKey': instance.encryptionKey,
'emailRecoveryRequested': instance.emailRecoveryRequested,
'receivedShares': instance.receivedShares,
};

View file

@ -40,9 +40,6 @@ class UserData {
@JsonKey(defaultValue: SetupProfile.standard) @JsonKey(defaultValue: SetupProfile.standard)
SetupProfile setupProfile = SetupProfile.standard; SetupProfile setupProfile = SetupProfile.standard;
@JsonKey(defaultValue: SecurityProfile.normal)
SecurityProfile securityProfile = SecurityProfile.normal;
// --- SUBSCRIPTION DTA --- // --- SUBSCRIPTION DTA ---
@JsonKey(defaultValue: 'Free') @JsonKey(defaultValue: 'Free')
@ -128,6 +125,9 @@ class UserData {
// -- Custom DATA -- // -- Custom DATA --
@JsonKey(defaultValue: true)
bool askForFriendPromotions = true;
@JsonKey(defaultValue: 100_000) @JsonKey(defaultValue: 100_000)
int currentPreKeyIndexStart = 100_000; int currentPreKeyIndexStart = 100_000;
@ -153,6 +153,8 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool isBackupEnabled = false; bool isBackupEnabled = false;
PasswordLessRecovery? passwordLessRecovery;
// Used for push notifcation via FCM. // Used for push notifcation via FCM.
String? fcmToken; String? fcmToken;
@ -200,3 +202,34 @@ class TwonlySafeBackup {
List<int> encryptionKey; List<int> encryptionKey;
Map<String, dynamic> toJson() => _$TwonlySafeBackupToJson(this); Map<String, dynamic> toJson() => _$TwonlySafeBackupToJson(this);
} }
@JsonSerializable()
class PasswordLessRecovery {
PasswordLessRecovery(this.threshold);
factory PasswordLessRecovery.fromJson(Map<String, dynamic> json) =>
_$PasswordLessRecoveryFromJson(json);
// Only stored, so the user can see his deposit email address...
String? email;
// <--
// Data shared with trusted friends
int threshold;
// Trusted friends are able to brute-force the pin -> Server delets after X tries
List<int>? pinSeed;
List<int>? pinUnlockToken;
// Stored not on the server, so the server is unable to link a email to a user until the actuall recovery or can
// brute-force the pin
List<int>? encryptedServerKeyNonce;
// --->
// Checking with the server that the server data is valid and not delted throug the pin protection for example.
DateTime? lastServerHeartbeat;
DateTime? lastContactHeartbeat;
List<int>? encryptedServerKey;
Map<String, dynamic> toJson() => _$PasswordLessRecoveryToJson(this);
}

View file

@ -23,12 +23,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..setupProfile = ..setupProfile =
$enumDecodeNullable(_$SetupProfileEnumMap, json['setupProfile']) ?? $enumDecodeNullable(_$SetupProfileEnumMap, json['setupProfile']) ??
SetupProfile.standard SetupProfile.standard
..securityProfile =
$enumDecodeNullable(
_$SecurityProfileEnumMap,
json['securityProfile'],
) ??
SecurityProfile.normal
..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String? ..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String?
..lastImageSend = json['lastImageSend'] == null ..lastImageSend = json['lastImageSend'] == null
? null ? null
@ -85,6 +79,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
json['userDiscoverySharePromotion'] as bool? ?? true json['userDiscoverySharePromotion'] as bool? ?? true
..userDiscoveryInitializationError = ..userDiscoveryInitializationError =
json['userDiscoveryInitializationError'] as bool? ?? false json['userDiscoveryInitializationError'] as bool? ?? false
..askForFriendPromotions = json['askForFriendPromotions'] as bool? ?? true
..currentPreKeyIndexStart = ..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart = ..currentSignedPreKeyIndexStart =
@ -102,6 +97,11 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
json['twonlySafeBackup'] as Map<String, dynamic>, json['twonlySafeBackup'] as Map<String, dynamic>,
) )
..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false
..passwordLessRecovery = json['passwordLessRecovery'] == null
? null
: PasswordLessRecovery.fromJson(
json['passwordLessRecovery'] as Map<String, dynamic>,
)
..fcmToken = json['fcmToken'] as String? ..fcmToken = json['fcmToken'] as String?
..askedForUserStudyPermission = ..askedForUserStudyPermission =
json['askedForUserStudyPermission'] as bool? ?? false json['askedForUserStudyPermission'] as bool? ?? false
@ -126,7 +126,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'isDeveloper': instance.isDeveloper, 'isDeveloper': instance.isDeveloper,
'deviceId': instance.deviceId, 'deviceId': instance.deviceId,
'setupProfile': _$SetupProfileEnumMap[instance.setupProfile]!, 'setupProfile': _$SetupProfileEnumMap[instance.setupProfile]!,
'securityProfile': _$SecurityProfileEnumMap[instance.securityProfile]!,
'subscriptionPlan': instance.subscriptionPlan, 'subscriptionPlan': instance.subscriptionPlan,
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore, 'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
'lastImageSend': instance.lastImageSend?.toIso8601String(), 'lastImageSend': instance.lastImageSend?.toIso8601String(),
@ -161,6 +160,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
instance.userDiscoveryRequiresManualApproval, instance.userDiscoveryRequiresManualApproval,
'userDiscoverySharePromotion': instance.userDiscoverySharePromotion, 'userDiscoverySharePromotion': instance.userDiscoverySharePromotion,
'userDiscoveryInitializationError': instance.userDiscoveryInitializationError, 'userDiscoveryInitializationError': instance.userDiscoveryInitializationError,
'askForFriendPromotions': instance.askForFriendPromotions,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash, 'lastChangeLogHash': instance.lastChangeLogHash,
@ -169,6 +169,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth, 'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth,
'twonlySafeBackup': instance.twonlySafeBackup, 'twonlySafeBackup': instance.twonlySafeBackup,
'isBackupEnabled': instance.isBackupEnabled, 'isBackupEnabled': instance.isBackupEnabled,
'passwordLessRecovery': instance.passwordLessRecovery,
'fcmToken': instance.fcmToken, 'fcmToken': instance.fcmToken,
'askedForUserStudyPermission': instance.askedForUserStudyPermission, 'askedForUserStudyPermission': instance.askedForUserStudyPermission,
'userStudyParticipantsToken': instance.userStudyParticipantsToken, 'userStudyParticipantsToken': instance.userStudyParticipantsToken,
@ -184,12 +185,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
const _$SetupProfileEnumMap = { const _$SetupProfileEnumMap = {
SetupProfile.standard: 'standard', SetupProfile.standard: 'standard',
SetupProfile.customized: 'customized', SetupProfile.customized: 'customized',
SetupProfile.maximum: 'maximum',
};
const _$SecurityProfileEnumMap = {
SecurityProfile.normal: 'normal',
SecurityProfile.strict: 'strict',
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
@ -232,3 +227,40 @@ const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.failed: 'failed', LastBackupUploadState.failed: 'failed',
LastBackupUploadState.success: 'success', LastBackupUploadState.success: 'success',
}; };
PasswordLessRecovery _$PasswordLessRecoveryFromJson(
Map<String, dynamic> json,
) => PasswordLessRecovery((json['threshold'] as num).toInt())
..email = json['email'] as String?
..pinSeed = (json['pinSeed'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..pinUnlockToken = (json['pinUnlockToken'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..encryptedServerKeyNonce =
(json['encryptedServerKeyNonce'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..lastServerHeartbeat = json['lastServerHeartbeat'] == null
? null
: DateTime.parse(json['lastServerHeartbeat'] as String)
..lastContactHeartbeat = json['lastContactHeartbeat'] == null
? null
: DateTime.parse(json['lastContactHeartbeat'] as String)
..encryptedServerKey = (json['encryptedServerKey'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList();
Map<String, dynamic> _$PasswordLessRecoveryToJson(
PasswordLessRecovery instance,
) => <String, dynamic>{
'email': instance.email,
'threshold': instance.threshold,
'pinSeed': instance.pinSeed,
'pinUnlockToken': instance.pinUnlockToken,
'encryptedServerKeyNonce': instance.encryptedServerKeyNonce,
'lastServerHeartbeat': instance.lastServerHeartbeat?.toIso8601String(),
'lastContactHeartbeat': instance.lastContactHeartbeat?.toIso8601String(),
'encryptedServerKey': instance.encryptedServerKey,
};

View file

@ -809,6 +809,301 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage {
void clearInBackground() => $_clearField(5); void clearInBackground() => $_clearField(5);
} }
class Handshake_GetServerKeyForPasswordLessRecovery
extends $pb.GeneratedMessage {
factory Handshake_GetServerKeyForPasswordLessRecovery({
$fixnum.Int64? userId,
$core.List<$core.int>? encryptedServerKeyNone,
$core.List<$core.int>? pinUnlockToken,
$core.List<$core.int>? pinProtectionKey,
$core.String? email,
}) {
final result = create();
if (userId != null) result.userId = userId;
if (encryptedServerKeyNone != null)
result.encryptedServerKeyNone = encryptedServerKeyNone;
if (pinUnlockToken != null) result.pinUnlockToken = pinUnlockToken;
if (pinProtectionKey != null) result.pinProtectionKey = pinProtectionKey;
if (email != null) result.email = email;
return result;
}
Handshake_GetServerKeyForPasswordLessRecovery._();
factory Handshake_GetServerKeyForPasswordLessRecovery.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Handshake_GetServerKeyForPasswordLessRecovery.fromJson(
$core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Handshake.GetServerKeyForPasswordLessRecovery',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'userId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'encryptedServerKeyNone', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
3, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'pinProtectionKey', $pb.PbFieldType.OY)
..aOS(5, _omitFieldNames ? '' : 'email')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_GetServerKeyForPasswordLessRecovery clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_GetServerKeyForPasswordLessRecovery copyWith(
void Function(Handshake_GetServerKeyForPasswordLessRecovery)
updates) =>
super.copyWith((message) =>
updates(message as Handshake_GetServerKeyForPasswordLessRecovery))
as Handshake_GetServerKeyForPasswordLessRecovery;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Handshake_GetServerKeyForPasswordLessRecovery create() =>
Handshake_GetServerKeyForPasswordLessRecovery._();
@$core.override
Handshake_GetServerKeyForPasswordLessRecovery createEmptyInstance() =>
create();
@$core.pragma('dart2js:noInline')
static Handshake_GetServerKeyForPasswordLessRecovery getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
Handshake_GetServerKeyForPasswordLessRecovery>(create);
static Handshake_GetServerKeyForPasswordLessRecovery? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get userId => $_getI64(0);
@$pb.TagNumber(1)
set userId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasUserId() => $_has(0);
@$pb.TagNumber(1)
void clearUserId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get encryptedServerKeyNone => $_getN(1);
@$pb.TagNumber(2)
set encryptedServerKeyNone($core.List<$core.int> value) =>
$_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasEncryptedServerKeyNone() => $_has(1);
@$pb.TagNumber(2)
void clearEncryptedServerKeyNone() => $_clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get pinUnlockToken => $_getN(2);
@$pb.TagNumber(3)
set pinUnlockToken($core.List<$core.int> value) => $_setBytes(2, value);
@$pb.TagNumber(3)
$core.bool hasPinUnlockToken() => $_has(2);
@$pb.TagNumber(3)
void clearPinUnlockToken() => $_clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get pinProtectionKey => $_getN(3);
@$pb.TagNumber(4)
set pinProtectionKey($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasPinProtectionKey() => $_has(3);
@$pb.TagNumber(4)
void clearPinProtectionKey() => $_clearField(4);
@$pb.TagNumber(5)
$core.String get email => $_getSZ(4);
@$pb.TagNumber(5)
set email($core.String value) => $_setString(4, value);
@$pb.TagNumber(5)
$core.bool hasEmail() => $_has(4);
@$pb.TagNumber(5)
void clearEmail() => $_clearField(5);
}
class Handshake_RegisterPasswordlessNotification extends $pb.GeneratedMessage {
factory Handshake_RegisterPasswordlessNotification({
$core.String? notificationId,
$core.List<$core.int>? downloadAuthToken,
$core.String? langCode,
$core.String? googleFcm,
}) {
final result = create();
if (notificationId != null) result.notificationId = notificationId;
if (downloadAuthToken != null) result.downloadAuthToken = downloadAuthToken;
if (langCode != null) result.langCode = langCode;
if (googleFcm != null) result.googleFcm = googleFcm;
return result;
}
Handshake_RegisterPasswordlessNotification._();
factory Handshake_RegisterPasswordlessNotification.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Handshake_RegisterPasswordlessNotification.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Handshake.RegisterPasswordlessNotification',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'notificationId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'downloadAuthToken', $pb.PbFieldType.OY)
..aOS(3, _omitFieldNames ? '' : 'langCode')
..aOS(4, _omitFieldNames ? '' : 'googleFcm')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_RegisterPasswordlessNotification clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_RegisterPasswordlessNotification copyWith(
void Function(Handshake_RegisterPasswordlessNotification) updates) =>
super.copyWith((message) =>
updates(message as Handshake_RegisterPasswordlessNotification))
as Handshake_RegisterPasswordlessNotification;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Handshake_RegisterPasswordlessNotification create() =>
Handshake_RegisterPasswordlessNotification._();
@$core.override
Handshake_RegisterPasswordlessNotification createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Handshake_RegisterPasswordlessNotification getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
Handshake_RegisterPasswordlessNotification>(create);
static Handshake_RegisterPasswordlessNotification? _defaultInstance;
@$pb.TagNumber(1)
$core.String get notificationId => $_getSZ(0);
@$pb.TagNumber(1)
set notificationId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasNotificationId() => $_has(0);
@$pb.TagNumber(1)
void clearNotificationId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get downloadAuthToken => $_getN(1);
@$pb.TagNumber(2)
set downloadAuthToken($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasDownloadAuthToken() => $_has(1);
@$pb.TagNumber(2)
void clearDownloadAuthToken() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get langCode => $_getSZ(2);
@$pb.TagNumber(3)
set langCode($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasLangCode() => $_has(2);
@$pb.TagNumber(3)
void clearLangCode() => $_clearField(3);
@$pb.TagNumber(4)
$core.String get googleFcm => $_getSZ(3);
@$pb.TagNumber(4)
set googleFcm($core.String value) => $_setString(3, value);
@$pb.TagNumber(4)
$core.bool hasGoogleFcm() => $_has(3);
@$pb.TagNumber(4)
void clearGoogleFcm() => $_clearField(4);
}
class Handshake_CheckForPasswordlessNotification extends $pb.GeneratedMessage {
factory Handshake_CheckForPasswordlessNotification({
$core.String? notificationId,
$core.List<$core.int>? downloadAuthToken,
$core.Iterable<$fixnum.Int64>? alreadyReceivedMessageIds,
}) {
final result = create();
if (notificationId != null) result.notificationId = notificationId;
if (downloadAuthToken != null) result.downloadAuthToken = downloadAuthToken;
if (alreadyReceivedMessageIds != null)
result.alreadyReceivedMessageIds.addAll(alreadyReceivedMessageIds);
return result;
}
Handshake_CheckForPasswordlessNotification._();
factory Handshake_CheckForPasswordlessNotification.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Handshake_CheckForPasswordlessNotification.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Handshake.CheckForPasswordlessNotification',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'notificationId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'downloadAuthToken', $pb.PbFieldType.OY)
..p<$fixnum.Int64>(3, _omitFieldNames ? '' : 'alreadyReceivedMessageIds',
$pb.PbFieldType.K6)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_CheckForPasswordlessNotification clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Handshake_CheckForPasswordlessNotification copyWith(
void Function(Handshake_CheckForPasswordlessNotification) updates) =>
super.copyWith((message) =>
updates(message as Handshake_CheckForPasswordlessNotification))
as Handshake_CheckForPasswordlessNotification;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Handshake_CheckForPasswordlessNotification create() =>
Handshake_CheckForPasswordlessNotification._();
@$core.override
Handshake_CheckForPasswordlessNotification createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Handshake_CheckForPasswordlessNotification getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
Handshake_CheckForPasswordlessNotification>(create);
static Handshake_CheckForPasswordlessNotification? _defaultInstance;
@$pb.TagNumber(1)
$core.String get notificationId => $_getSZ(0);
@$pb.TagNumber(1)
set notificationId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasNotificationId() => $_has(0);
@$pb.TagNumber(1)
void clearNotificationId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get downloadAuthToken => $_getN(1);
@$pb.TagNumber(2)
set downloadAuthToken($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasDownloadAuthToken() => $_has(1);
@$pb.TagNumber(2)
void clearDownloadAuthToken() => $_clearField(2);
@$pb.TagNumber(3)
$pb.PbList<$fixnum.Int64> get alreadyReceivedMessageIds => $_getList(2);
}
enum Handshake_Handshake { enum Handshake_Handshake {
register, register,
getAuthChallenge, getAuthChallenge,
@ -817,6 +1112,9 @@ enum Handshake_Handshake {
requestPOW, requestPOW,
authenticateWithLoginToken, authenticateWithLoginToken,
getUseridByUsername, getUseridByUsername,
getServerKeyForPasswordlessRecovery,
registerPasswordlessNotification,
checkForPasswordlessNotification,
notSet notSet
} }
@ -829,6 +1127,12 @@ class Handshake extends $pb.GeneratedMessage {
Handshake_RequestPOW? requestPOW, Handshake_RequestPOW? requestPOW,
Handshake_AuthenticateWithLoginToken? authenticateWithLoginToken, Handshake_AuthenticateWithLoginToken? authenticateWithLoginToken,
Handshake_GetUserIdByUsername? getUseridByUsername, Handshake_GetUserIdByUsername? getUseridByUsername,
Handshake_GetServerKeyForPasswordLessRecovery?
getServerKeyForPasswordlessRecovery,
Handshake_RegisterPasswordlessNotification?
registerPasswordlessNotification,
Handshake_CheckForPasswordlessNotification?
checkForPasswordlessNotification,
}) { }) {
final result = create(); final result = create();
if (register != null) result.register = register; if (register != null) result.register = register;
@ -840,6 +1144,15 @@ class Handshake extends $pb.GeneratedMessage {
result.authenticateWithLoginToken = authenticateWithLoginToken; result.authenticateWithLoginToken = authenticateWithLoginToken;
if (getUseridByUsername != null) if (getUseridByUsername != null)
result.getUseridByUsername = getUseridByUsername; result.getUseridByUsername = getUseridByUsername;
if (getServerKeyForPasswordlessRecovery != null)
result.getServerKeyForPasswordlessRecovery =
getServerKeyForPasswordlessRecovery;
if (registerPasswordlessNotification != null)
result.registerPasswordlessNotification =
registerPasswordlessNotification;
if (checkForPasswordlessNotification != null)
result.checkForPasswordlessNotification =
checkForPasswordlessNotification;
return result; return result;
} }
@ -861,6 +1174,9 @@ class Handshake extends $pb.GeneratedMessage {
5: Handshake_Handshake.requestPOW, 5: Handshake_Handshake.requestPOW,
6: Handshake_Handshake.authenticateWithLoginToken, 6: Handshake_Handshake.authenticateWithLoginToken,
7: Handshake_Handshake.getUseridByUsername, 7: Handshake_Handshake.getUseridByUsername,
8: Handshake_Handshake.getServerKeyForPasswordlessRecovery,
9: Handshake_Handshake.registerPasswordlessNotification,
10: Handshake_Handshake.checkForPasswordlessNotification,
0: Handshake_Handshake.notSet 0: Handshake_Handshake.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -868,7 +1184,7 @@ class Handshake extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5, 6, 7]) ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
..aOM<Handshake_Register>(1, _omitFieldNames ? '' : 'register', ..aOM<Handshake_Register>(1, _omitFieldNames ? '' : 'register',
subBuilder: Handshake_Register.create) subBuilder: Handshake_Register.create)
..aOM<Handshake_GetAuthChallenge>( ..aOM<Handshake_GetAuthChallenge>(
@ -887,6 +1203,15 @@ class Handshake extends $pb.GeneratedMessage {
..aOM<Handshake_GetUserIdByUsername>( ..aOM<Handshake_GetUserIdByUsername>(
7, _omitFieldNames ? '' : 'getUseridByUsername', 7, _omitFieldNames ? '' : 'getUseridByUsername',
subBuilder: Handshake_GetUserIdByUsername.create) subBuilder: Handshake_GetUserIdByUsername.create)
..aOM<Handshake_GetServerKeyForPasswordLessRecovery>(
8, _omitFieldNames ? '' : 'getServerKeyForPasswordlessRecovery',
subBuilder: Handshake_GetServerKeyForPasswordLessRecovery.create)
..aOM<Handshake_RegisterPasswordlessNotification>(
9, _omitFieldNames ? '' : 'registerPasswordlessNotification',
subBuilder: Handshake_RegisterPasswordlessNotification.create)
..aOM<Handshake_CheckForPasswordlessNotification>(
10, _omitFieldNames ? '' : 'checkForPasswordlessNotification',
subBuilder: Handshake_CheckForPasswordlessNotification.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -914,6 +1239,9 @@ class Handshake extends $pb.GeneratedMessage {
@$pb.TagNumber(5) @$pb.TagNumber(5)
@$pb.TagNumber(6) @$pb.TagNumber(6)
@$pb.TagNumber(7) @$pb.TagNumber(7)
@$pb.TagNumber(8)
@$pb.TagNumber(9)
@$pb.TagNumber(10)
Handshake_Handshake whichHandshake() => Handshake_Handshake whichHandshake() =>
_Handshake_HandshakeByTag[$_whichOneof(0)]!; _Handshake_HandshakeByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -923,6 +1251,9 @@ class Handshake extends $pb.GeneratedMessage {
@$pb.TagNumber(5) @$pb.TagNumber(5)
@$pb.TagNumber(6) @$pb.TagNumber(6)
@$pb.TagNumber(7) @$pb.TagNumber(7)
@$pb.TagNumber(8)
@$pb.TagNumber(9)
@$pb.TagNumber(10)
void clearHandshake() => $_clearField($_whichOneof(0)); void clearHandshake() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -1006,6 +1337,51 @@ class Handshake extends $pb.GeneratedMessage {
void clearGetUseridByUsername() => $_clearField(7); void clearGetUseridByUsername() => $_clearField(7);
@$pb.TagNumber(7) @$pb.TagNumber(7)
Handshake_GetUserIdByUsername ensureGetUseridByUsername() => $_ensure(6); Handshake_GetUserIdByUsername ensureGetUseridByUsername() => $_ensure(6);
@$pb.TagNumber(8)
Handshake_GetServerKeyForPasswordLessRecovery
get getServerKeyForPasswordlessRecovery => $_getN(7);
@$pb.TagNumber(8)
set getServerKeyForPasswordlessRecovery(
Handshake_GetServerKeyForPasswordLessRecovery value) =>
$_setField(8, value);
@$pb.TagNumber(8)
$core.bool hasGetServerKeyForPasswordlessRecovery() => $_has(7);
@$pb.TagNumber(8)
void clearGetServerKeyForPasswordlessRecovery() => $_clearField(8);
@$pb.TagNumber(8)
Handshake_GetServerKeyForPasswordLessRecovery
ensureGetServerKeyForPasswordlessRecovery() => $_ensure(7);
@$pb.TagNumber(9)
Handshake_RegisterPasswordlessNotification
get registerPasswordlessNotification => $_getN(8);
@$pb.TagNumber(9)
set registerPasswordlessNotification(
Handshake_RegisterPasswordlessNotification value) =>
$_setField(9, value);
@$pb.TagNumber(9)
$core.bool hasRegisterPasswordlessNotification() => $_has(8);
@$pb.TagNumber(9)
void clearRegisterPasswordlessNotification() => $_clearField(9);
@$pb.TagNumber(9)
Handshake_RegisterPasswordlessNotification
ensureRegisterPasswordlessNotification() => $_ensure(8);
@$pb.TagNumber(10)
Handshake_CheckForPasswordlessNotification
get checkForPasswordlessNotification => $_getN(9);
@$pb.TagNumber(10)
set checkForPasswordlessNotification(
Handshake_CheckForPasswordlessNotification value) =>
$_setField(10, value);
@$pb.TagNumber(10)
$core.bool hasCheckForPasswordlessNotification() => $_has(9);
@$pb.TagNumber(10)
void clearCheckForPasswordlessNotification() => $_clearField(10);
@$pb.TagNumber(10)
Handshake_CheckForPasswordlessNotification
ensureCheckForPasswordlessNotification() => $_ensure(9);
} }
class ApplicationData_TextMessage extends $pb.GeneratedMessage { class ApplicationData_TextMessage extends $pb.GeneratedMessage {
@ -2182,6 +2558,160 @@ class ApplicationData_Deprecated extends $pb.GeneratedMessage {
static ApplicationData_Deprecated? _defaultInstance; static ApplicationData_Deprecated? _defaultInstance;
} }
class ApplicationData_RegisterPasswordLessRecovery
extends $pb.GeneratedMessage {
factory ApplicationData_RegisterPasswordLessRecovery({
$core.List<$core.int>? encryptedServerKey,
$core.List<$core.int>? pinUnlockToken,
}) {
final result = create();
if (encryptedServerKey != null)
result.encryptedServerKey = encryptedServerKey;
if (pinUnlockToken != null) result.pinUnlockToken = pinUnlockToken;
return result;
}
ApplicationData_RegisterPasswordLessRecovery._();
factory ApplicationData_RegisterPasswordLessRecovery.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ApplicationData_RegisterPasswordLessRecovery.fromJson(
$core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ApplicationData.RegisterPasswordLessRecovery',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'encryptedServerKey', $pb.PbFieldType.OY,
protoName: 'encryptedServerKey')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY,
protoName: 'pinUnlockToken')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_RegisterPasswordLessRecovery clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_RegisterPasswordLessRecovery copyWith(
void Function(ApplicationData_RegisterPasswordLessRecovery)
updates) =>
super.copyWith((message) =>
updates(message as ApplicationData_RegisterPasswordLessRecovery))
as ApplicationData_RegisterPasswordLessRecovery;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ApplicationData_RegisterPasswordLessRecovery create() =>
ApplicationData_RegisterPasswordLessRecovery._();
@$core.override
ApplicationData_RegisterPasswordLessRecovery createEmptyInstance() =>
create();
@$core.pragma('dart2js:noInline')
static ApplicationData_RegisterPasswordLessRecovery getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
ApplicationData_RegisterPasswordLessRecovery>(create);
static ApplicationData_RegisterPasswordLessRecovery? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get encryptedServerKey => $_getN(0);
@$pb.TagNumber(1)
set encryptedServerKey($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasEncryptedServerKey() => $_has(0);
@$pb.TagNumber(1)
void clearEncryptedServerKey() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get pinUnlockToken => $_getN(1);
@$pb.TagNumber(2)
set pinUnlockToken($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasPinUnlockToken() => $_has(1);
@$pb.TagNumber(2)
void clearPinUnlockToken() => $_clearField(2);
}
class ApplicationData_PasswordlessNotification extends $pb.GeneratedMessage {
factory ApplicationData_PasswordlessNotification({
$core.String? notificationId,
$core.List<$core.int>? encryptedMessage,
}) {
final result = create();
if (notificationId != null) result.notificationId = notificationId;
if (encryptedMessage != null) result.encryptedMessage = encryptedMessage;
return result;
}
ApplicationData_PasswordlessNotification._();
factory ApplicationData_PasswordlessNotification.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ApplicationData_PasswordlessNotification.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ApplicationData.PasswordlessNotification',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'notificationId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'encryptedMessage', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_PasswordlessNotification clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_PasswordlessNotification copyWith(
void Function(ApplicationData_PasswordlessNotification) updates) =>
super.copyWith((message) =>
updates(message as ApplicationData_PasswordlessNotification))
as ApplicationData_PasswordlessNotification;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ApplicationData_PasswordlessNotification create() =>
ApplicationData_PasswordlessNotification._();
@$core.override
ApplicationData_PasswordlessNotification createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ApplicationData_PasswordlessNotification getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
ApplicationData_PasswordlessNotification>(create);
static ApplicationData_PasswordlessNotification? _defaultInstance;
@$pb.TagNumber(1)
$core.String get notificationId => $_getSZ(0);
@$pb.TagNumber(1)
set notificationId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasNotificationId() => $_has(0);
@$pb.TagNumber(1)
void clearNotificationId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get encryptedMessage => $_getN(1);
@$pb.TagNumber(2)
set encryptedMessage($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasEncryptedMessage() => $_has(1);
@$pb.TagNumber(2)
void clearEncryptedMessage() => $_clearField(2);
}
enum ApplicationData_ApplicationData { enum ApplicationData_ApplicationData {
textMessage, textMessage,
getUserByUsername, getUserByUsername,
@ -2209,6 +2739,8 @@ enum ApplicationData_ApplicationData {
ipaForceCheck, ipaForceCheck,
addAdditionalUser, addAdditionalUser,
setLoginToken, setLoginToken,
registerPasswordlessRecovery,
passwordlessNotification,
notSet notSet
} }
@ -2240,6 +2772,8 @@ class ApplicationData extends $pb.GeneratedMessage {
ApplicationData_IPAForceCheck? ipaForceCheck, ApplicationData_IPAForceCheck? ipaForceCheck,
ApplicationData_AddAdditionalUser? addAdditionalUser, ApplicationData_AddAdditionalUser? addAdditionalUser,
ApplicationData_SetLoginToken? setLoginToken, ApplicationData_SetLoginToken? setLoginToken,
ApplicationData_RegisterPasswordLessRecovery? registerPasswordlessRecovery,
ApplicationData_PasswordlessNotification? passwordlessNotification,
}) { }) {
final result = create(); final result = create();
if (textMessage != null) result.textMessage = textMessage; if (textMessage != null) result.textMessage = textMessage;
@ -2274,6 +2808,10 @@ class ApplicationData extends $pb.GeneratedMessage {
if (ipaForceCheck != null) result.ipaForceCheck = ipaForceCheck; if (ipaForceCheck != null) result.ipaForceCheck = ipaForceCheck;
if (addAdditionalUser != null) result.addAdditionalUser = addAdditionalUser; if (addAdditionalUser != null) result.addAdditionalUser = addAdditionalUser;
if (setLoginToken != null) result.setLoginToken = setLoginToken; if (setLoginToken != null) result.setLoginToken = setLoginToken;
if (registerPasswordlessRecovery != null)
result.registerPasswordlessRecovery = registerPasswordlessRecovery;
if (passwordlessNotification != null)
result.passwordlessNotification = passwordlessNotification;
return result; return result;
} }
@ -2314,6 +2852,8 @@ class ApplicationData extends $pb.GeneratedMessage {
28: ApplicationData_ApplicationData.ipaForceCheck, 28: ApplicationData_ApplicationData.ipaForceCheck,
29: ApplicationData_ApplicationData.addAdditionalUser, 29: ApplicationData_ApplicationData.addAdditionalUser,
30: ApplicationData_ApplicationData.setLoginToken, 30: ApplicationData_ApplicationData.setLoginToken,
31: ApplicationData_ApplicationData.registerPasswordlessRecovery,
32: ApplicationData_ApplicationData.passwordlessNotification,
0: ApplicationData_ApplicationData.notSet 0: ApplicationData_ApplicationData.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -2347,7 +2887,9 @@ class ApplicationData extends $pb.GeneratedMessage {
27, 27,
28, 28,
29, 29,
30 30,
31,
32
]) ])
..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage', ..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage',
protoName: 'textMessage', protoName: 'textMessage',
@ -2439,6 +2981,12 @@ class ApplicationData extends $pb.GeneratedMessage {
..aOM<ApplicationData_SetLoginToken>( ..aOM<ApplicationData_SetLoginToken>(
30, _omitFieldNames ? '' : 'setLoginToken', 30, _omitFieldNames ? '' : 'setLoginToken',
subBuilder: ApplicationData_SetLoginToken.create) subBuilder: ApplicationData_SetLoginToken.create)
..aOM<ApplicationData_RegisterPasswordLessRecovery>(
31, _omitFieldNames ? '' : 'registerPasswordlessRecovery',
subBuilder: ApplicationData_RegisterPasswordLessRecovery.create)
..aOM<ApplicationData_PasswordlessNotification>(
32, _omitFieldNames ? '' : 'passwordlessNotification',
subBuilder: ApplicationData_PasswordlessNotification.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -2486,6 +3034,8 @@ class ApplicationData extends $pb.GeneratedMessage {
@$pb.TagNumber(28) @$pb.TagNumber(28)
@$pb.TagNumber(29) @$pb.TagNumber(29)
@$pb.TagNumber(30) @$pb.TagNumber(30)
@$pb.TagNumber(31)
@$pb.TagNumber(32)
ApplicationData_ApplicationData whichApplicationData() => ApplicationData_ApplicationData whichApplicationData() =>
_ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!; _ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -2514,6 +3064,8 @@ class ApplicationData extends $pb.GeneratedMessage {
@$pb.TagNumber(28) @$pb.TagNumber(28)
@$pb.TagNumber(29) @$pb.TagNumber(29)
@$pb.TagNumber(30) @$pb.TagNumber(30)
@$pb.TagNumber(31)
@$pb.TagNumber(32)
void clearApplicationData() => $_clearField($_whichOneof(0)); void clearApplicationData() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -2819,6 +3371,36 @@ class ApplicationData extends $pb.GeneratedMessage {
void clearSetLoginToken() => $_clearField(30); void clearSetLoginToken() => $_clearField(30);
@$pb.TagNumber(30) @$pb.TagNumber(30)
ApplicationData_SetLoginToken ensureSetLoginToken() => $_ensure(25); ApplicationData_SetLoginToken ensureSetLoginToken() => $_ensure(25);
@$pb.TagNumber(31)
ApplicationData_RegisterPasswordLessRecovery
get registerPasswordlessRecovery => $_getN(26);
@$pb.TagNumber(31)
set registerPasswordlessRecovery(
ApplicationData_RegisterPasswordLessRecovery value) =>
$_setField(31, value);
@$pb.TagNumber(31)
$core.bool hasRegisterPasswordlessRecovery() => $_has(26);
@$pb.TagNumber(31)
void clearRegisterPasswordlessRecovery() => $_clearField(31);
@$pb.TagNumber(31)
ApplicationData_RegisterPasswordLessRecovery
ensureRegisterPasswordlessRecovery() => $_ensure(26);
@$pb.TagNumber(32)
ApplicationData_PasswordlessNotification get passwordlessNotification =>
$_getN(27);
@$pb.TagNumber(32)
set passwordlessNotification(
ApplicationData_PasswordlessNotification value) =>
$_setField(32, value);
@$pb.TagNumber(32)
$core.bool hasPasswordlessNotification() => $_has(27);
@$pb.TagNumber(32)
void clearPasswordlessNotification() => $_clearField(32);
@$pb.TagNumber(32)
ApplicationData_PasswordlessNotification ensurePasswordlessNotification() =>
$_ensure(27);
} }
class Response_PreKey extends $pb.GeneratedMessage { class Response_PreKey extends $pb.GeneratedMessage {

View file

@ -152,6 +152,33 @@ const Handshake$json = {
'9': 0, '9': 0,
'10': 'getUseridByUsername' '10': 'getUseridByUsername'
}, },
{
'1': 'get_server_key_for_passwordless_recovery',
'3': 8,
'4': 1,
'5': 11,
'6': '.client_to_server.Handshake.GetServerKeyForPasswordLessRecovery',
'9': 0,
'10': 'getServerKeyForPasswordlessRecovery'
},
{
'1': 'register_passwordless_notification',
'3': 9,
'4': 1,
'5': 11,
'6': '.client_to_server.Handshake.RegisterPasswordlessNotification',
'9': 0,
'10': 'registerPasswordlessNotification'
},
{
'1': 'check_for_passwordless_notification',
'3': 10,
'4': 1,
'5': 11,
'6': '.client_to_server.Handshake.CheckForPasswordlessNotification',
'9': 0,
'10': 'checkForPasswordlessNotification'
},
], ],
'3': [ '3': [
Handshake_RequestPOW$json, Handshake_RequestPOW$json,
@ -160,7 +187,10 @@ const Handshake$json = {
Handshake_GetUserIdByUsername$json, Handshake_GetUserIdByUsername$json,
Handshake_GetAuthToken$json, Handshake_GetAuthToken$json,
Handshake_Authenticate$json, Handshake_Authenticate$json,
Handshake_AuthenticateWithLoginToken$json Handshake_AuthenticateWithLoginToken$json,
Handshake_GetServerKeyForPasswordLessRecovery$json,
Handshake_RegisterPasswordlessNotification$json,
Handshake_CheckForPasswordlessNotification$json
], ],
'8': [ '8': [
{'1': 'Handshake'}, {'1': 'Handshake'},
@ -303,6 +333,95 @@ const Handshake_AuthenticateWithLoginToken$json = {
], ],
}; };
@$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_GetServerKeyForPasswordLessRecovery$json = {
'1': 'GetServerKeyForPasswordLessRecovery',
'2': [
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
{
'1': 'encrypted_server_key_none',
'3': 2,
'4': 1,
'5': 12,
'10': 'encryptedServerKeyNone'
},
{
'1': 'pin_unlock_token',
'3': 3,
'4': 1,
'5': 12,
'9': 0,
'10': 'pinUnlockToken',
'17': true
},
{
'1': 'pin_protection_key',
'3': 4,
'4': 1,
'5': 12,
'9': 1,
'10': 'pinProtectionKey',
'17': true
},
{'1': 'email', '3': 5, '4': 1, '5': 9, '9': 2, '10': 'email', '17': true},
],
'8': [
{'1': '_pin_unlock_token'},
{'1': '_pin_protection_key'},
{'1': '_email'},
],
};
@$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_RegisterPasswordlessNotification$json = {
'1': 'RegisterPasswordlessNotification',
'2': [
{'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'},
{
'1': 'download_auth_token',
'3': 2,
'4': 1,
'5': 12,
'10': 'downloadAuthToken'
},
{'1': 'lang_code', '3': 3, '4': 1, '5': 9, '10': 'langCode'},
{
'1': 'google_fcm',
'3': 4,
'4': 1,
'5': 9,
'9': 0,
'10': 'googleFcm',
'17': true
},
],
'8': [
{'1': '_google_fcm'},
],
};
@$core.Deprecated('Use handshakeDescriptor instead')
const Handshake_CheckForPasswordlessNotification$json = {
'1': 'CheckForPasswordlessNotification',
'2': [
{'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'},
{
'1': 'download_auth_token',
'3': 2,
'4': 1,
'5': 12,
'10': 'downloadAuthToken'
},
{
'1': 'already_received_message_ids',
'3': 3,
'4': 3,
'5': 3,
'10': 'alreadyReceivedMessageIds'
},
],
};
/// Descriptor for `Handshake`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `Handshake`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG' 'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG'
@ -316,26 +435,47 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode(
'MjYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW' 'MjYuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuQXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW'
'5IAFIaYXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW4SZgoWZ2V0X3VzZXJpZF9ieV91c2VybmFt' '5IAFIaYXV0aGVudGljYXRlV2l0aExvZ2luVG9rZW4SZgoWZ2V0X3VzZXJpZF9ieV91c2VybmFt'
'ZRgHIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFVzZXJJZEJ5VXNlcm5hbW' 'ZRgHIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFVzZXJJZEJ5VXNlcm5hbW'
'VIAFITZ2V0VXNlcmlkQnlVc2VybmFtZRoMCgpSZXF1ZXN0UE9XGsoDCghSZWdpc3RlchIaCgh1' 'VIAFITZ2V0VXNlcmlkQnlVc2VybmFtZRKYAQooZ2V0X3NlcnZlcl9rZXlfZm9yX3Bhc3N3b3Jk'
'c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUSJAoLaW52aXRlX2NvZGUYAiABKAlIAFIKaW52aXRlQ2' 'bGVzc19yZWNvdmVyeRgIIAEoCzI/LmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFNlcn'
'9kZYgBARIuChNwdWJsaWNfaWRlbnRpdHlfa2V5GAMgASgMUhFwdWJsaWNJZGVudGl0eUtleRIj' 'ZlcktleUZvclBhc3N3b3JkTGVzc1JlY292ZXJ5SABSI2dldFNlcnZlcktleUZvclBhc3N3b3Jk'
'Cg1zaWduZWRfcHJla2V5GAQgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaW' 'bGVzc1JlY292ZXJ5EowBCiJyZWdpc3Rlcl9wYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uGAkgAS'
'duYXR1cmUYBSABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lk' 'gLMjwuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVnaXN0ZXJQYXNzd29yZGxlc3NOb3Rp'
'GAYgASgDUg5zaWduZWRQcmVrZXlJZBInCg9yZWdpc3RyYXRpb25faWQYByABKANSDnJlZ2lzdH' 'ZmljYXRpb25IAFIgcmVnaXN0ZXJQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SjQEKI2NoZWNrX2'
'JhdGlvbklkEhUKBmlzX2lvcxgIIAEoCFIFaXNJb3MSGwoJbGFuZ19jb2RlGAkgASgJUghsYW5n' 'Zvcl9wYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uGAogASgLMjwuY2xpZW50X3RvX3NlcnZlci5I'
'Q29kZRIiCg1wcm9vZl9vZl93b3JrGAogASgDUgtwcm9vZk9mV29yaxIkCgtsb2dpbl90b2tlbh' 'YW5kc2hha2UuQ2hlY2tGb3JQYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIgY2hlY2tGb3JQYX'
'gLIAEoDEgBUgpsb2dpblRva2VuiAEBQg4KDF9pbnZpdGVfY29kZUIOCgxfbG9naW5fdG9rZW4a' 'Nzd29yZGxlc3NOb3RpZmljYXRpb24aDAoKUmVxdWVzdFBPVxrKAwoIUmVnaXN0ZXISGgoIdXNl'
'EgoQR2V0QXV0aENoYWxsZW5nZRoxChNHZXRVc2VySWRCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA' 'cm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGAIgASgJSABSCmludml0ZUNvZG'
'EgASgJUgh1c2VybmFtZRpDCgxHZXRBdXRoVG9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklk' 'WIAQESLgoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDFIRcHVibGljSWRlbnRpdHlLZXkSIwoN'
'EhoKCHJlc3BvbnNlGAIgASgMUghyZXNwb25zZRroAQoMQXV0aGVudGljYXRlEhcKB3VzZXJfaW' 'c2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbm'
'QYASABKANSBnVzZXJJZBIdCgphdXRoX3Rva2VuGAIgASgMUglhdXRoVG9rZW4SJAoLYXBwX3Zl' 'F0dXJlGAUgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUSKAoQc2lnbmVkX3ByZWtleV9pZBgG'
'cnNpb24YAyABKAlIAFIKYXBwVmVyc2lvbogBARIgCglkZXZpY2VfaWQYBCABKANIAVIIZGV2aW' 'IAEoA1IOc2lnbmVkUHJla2V5SWQSJwoPcmVnaXN0cmF0aW9uX2lkGAcgASgDUg5yZWdpc3RyYX'
'NlSWSIAQESKAoNaW5fYmFja2dyb3VuZBgFIAEoCEgCUgxpbkJhY2tncm91bmSIAQFCDgoMX2Fw' 'Rpb25JZBIVCgZpc19pb3MYCCABKAhSBWlzSW9zEhsKCWxhbmdfY29kZRgJIAEoCVIIbGFuZ0Nv'
'cF92ZXJzaW9uQgwKCl9kZXZpY2VfaWRCEAoOX2luX2JhY2tncm91bmQaxgEKGkF1dGhlbnRpY2' 'ZGUSIgoNcHJvb2Zfb2Zfd29yaxgKIAEoA1ILcHJvb2ZPZldvcmsSJAoLbG9naW5fdG9rZW4YCy'
'F0ZVdpdGhMb2dpblRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIsChJzZWNyZXRfbG9n' 'ABKAxIAVIKbG9naW5Ub2tlbogBAUIOCgxfaW52aXRlX2NvZGVCDgoMX2xvZ2luX3Rva2VuGhIK'
'aW5fdG9rZW4YAiABKAxSEHNlY3JldExvZ2luVG9rZW4SHwoLYXBwX3ZlcnNpb24YAyABKAlSCm' 'EEdldEF1dGhDaGFsbGVuZ2UaMQoTR2V0VXNlcklkQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIA'
'FwcFZlcnNpb24SGwoJZGV2aWNlX2lkGAQgASgDUghkZXZpY2VJZBIjCg1pbl9iYWNrZ3JvdW5k' 'EoCVIIdXNlcm5hbWUaQwoMR2V0QXV0aFRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIa'
'GAUgASgIUgxpbkJhY2tncm91bmRCCwoJSGFuZHNoYWtl'); 'CghyZXNwb25zZRgCIAEoDFIIcmVzcG9uc2Ua6AEKDEF1dGhlbnRpY2F0ZRIXCgd1c2VyX2lkGA'
'EgASgDUgZ1c2VySWQSHQoKYXV0aF90b2tlbhgCIAEoDFIJYXV0aFRva2VuEiQKC2FwcF92ZXJz'
'aW9uGAMgASgJSABSCmFwcFZlcnNpb26IAQESIAoJZGV2aWNlX2lkGAQgASgDSAFSCGRldmljZU'
'lkiAEBEigKDWluX2JhY2tncm91bmQYBSABKAhIAlIMaW5CYWNrZ3JvdW5kiAEBQg4KDF9hcHBf'
'dmVyc2lvbkIMCgpfZGV2aWNlX2lkQhAKDl9pbl9iYWNrZ3JvdW5kGsYBChpBdXRoZW50aWNhdG'
'VXaXRoTG9naW5Ub2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSLAoSc2VjcmV0X2xvZ2lu'
'X3Rva2VuGAIgASgMUhBzZWNyZXRMb2dpblRva2VuEh8KC2FwcF92ZXJzaW9uGAMgASgJUgphcH'
'BWZXJzaW9uEhsKCWRldmljZV9pZBgEIAEoA1IIZGV2aWNlSWQSIwoNaW5fYmFja2dyb3VuZBgF'
'IAEoCFIMaW5CYWNrZ3JvdW5kGqwCCiNHZXRTZXJ2ZXJLZXlGb3JQYXNzd29yZExlc3NSZWNvdm'
'VyeRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOQoZZW5jcnlwdGVkX3NlcnZlcl9rZXlfbm9u'
'ZRgCIAEoDFIWZW5jcnlwdGVkU2VydmVyS2V5Tm9uZRItChBwaW5fdW5sb2NrX3Rva2VuGAMgAS'
'gMSABSDnBpblVubG9ja1Rva2VuiAEBEjEKEnBpbl9wcm90ZWN0aW9uX2tleRgEIAEoDEgBUhBw'
'aW5Qcm90ZWN0aW9uS2V5iAEBEhkKBWVtYWlsGAUgASgJSAJSBWVtYWlsiAEBQhMKEV9waW5fdW'
'5sb2NrX3Rva2VuQhUKE19waW5fcHJvdGVjdGlvbl9rZXlCCAoGX2VtYWlsGssBCiBSZWdpc3Rl'
'clBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYXRpb25faWQYASABKAlSDm5vdG'
'lmaWNhdGlvbklkEi4KE2Rvd25sb2FkX2F1dGhfdG9rZW4YAiABKAxSEWRvd25sb2FkQXV0aFRv'
'a2VuEhsKCWxhbmdfY29kZRgDIAEoCVIIbGFuZ0NvZGUSIgoKZ29vZ2xlX2ZjbRgEIAEoCUgAUg'
'lnb29nbGVGY22IAQFCDQoLX2dvb2dsZV9mY20avAEKIENoZWNrRm9yUGFzc3dvcmRsZXNzTm90'
'aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW9uSWQSLgoTZG'
'93bmxvYWRfYXV0aF90b2tlbhgCIAEoDFIRZG93bmxvYWRBdXRoVG9rZW4SPwocYWxyZWFkeV9y'
'ZWNlaXZlZF9tZXNzYWdlX2lkcxgDIAMoA1IZYWxyZWFkeVJlY2VpdmVkTWVzc2FnZUlkc0ILCg'
'lIYW5kc2hha2U=');
@$core.Deprecated('Use applicationDataDescriptor instead') @$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData$json = { const ApplicationData$json = {
@ -575,6 +715,24 @@ const ApplicationData$json = {
'9': 0, '9': 0,
'10': 'setLoginToken' '10': 'setLoginToken'
}, },
{
'1': 'register_passwordless_recovery',
'3': 31,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.RegisterPasswordLessRecovery',
'9': 0,
'10': 'registerPasswordlessRecovery'
},
{
'1': 'passwordless_notification',
'3': 32,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.PasswordlessNotification',
'9': 0,
'10': 'passwordlessNotification'
},
], ],
'3': [ '3': [
ApplicationData_TextMessage$json, ApplicationData_TextMessage$json,
@ -596,7 +754,9 @@ const ApplicationData$json = {
ApplicationData_DeleteAccount$json, ApplicationData_DeleteAccount$json,
ApplicationData_AddAdditionalUser$json, ApplicationData_AddAdditionalUser$json,
ApplicationData_SetLoginToken$json, ApplicationData_SetLoginToken$json,
ApplicationData_Deprecated$json ApplicationData_Deprecated$json,
ApplicationData_RegisterPasswordLessRecovery$json,
ApplicationData_PasswordlessNotification$json
], ],
'8': [ '8': [
{'1': 'ApplicationData'}, {'1': 'ApplicationData'},
@ -775,6 +935,47 @@ const ApplicationData_Deprecated$json = {
'1': 'Deprecated', '1': 'Deprecated',
}; };
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_RegisterPasswordLessRecovery$json = {
'1': 'RegisterPasswordLessRecovery',
'2': [
{
'1': 'encryptedServerKey',
'3': 1,
'4': 1,
'5': 12,
'10': 'encryptedServerKey'
},
{
'1': 'pinUnlockToken',
'3': 2,
'4': 1,
'5': 12,
'9': 0,
'10': 'pinUnlockToken',
'17': true
},
],
'8': [
{'1': '_pinUnlockToken'},
],
};
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_PasswordlessNotification$json = {
'1': 'PasswordlessNotification',
'2': [
{'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'},
{
'1': 'encrypted_message',
'3': 2,
'4': 1,
'5': 12,
'10': 'encryptedMessage'
},
],
};
/// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm'
@ -820,27 +1021,36 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'dmVBZGRpdGlvbmFsVXNlchJjChFhZGRBZGRpdGlvbmFsVXNlchgdIAEoCzIzLmNsaWVudF90b1' 'dmVBZGRpdGlvbmFsVXNlchJjChFhZGRBZGRpdGlvbmFsVXNlchgdIAEoCzIzLmNsaWVudF90b1'
'9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkFkZEFkZGl0aW9uYWxVc2VySABSEWFkZEFkZGl0aW9u' '9zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkFkZEFkZGl0aW9uYWxVc2VySABSEWFkZEFkZGl0aW9u'
'YWxVc2VyElkKD3NldF9sb2dpbl90b2tlbhgeIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG' 'YWxVc2VyElkKD3NldF9sb2dpbl90b2tlbhgeIAEoCzIvLmNsaWVudF90b19zZXJ2ZXIuQXBwbG'
'ljYXRpb25EYXRhLlNldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhpqCgtUZXh0TWVzc2Fn' 'ljYXRpb25EYXRhLlNldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhKGAQoecmVnaXN0ZXJf'
'ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEoDFIEYm9keRIgCglwdXNoX2' 'cGFzc3dvcmRsZXNzX3JlY292ZXJ5GB8gASgLMj4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG'
'RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YRovChFHZXRVc2VyQnlVc2Vy' 'lvbkRhdGEuUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeUgAUhxyZWdpc3RlclBhc3N3b3Jk'
'bmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbWUSGgoIdX' 'bGVzc1JlY292ZXJ5EnkKGXBhc3N3b3JkbGVzc19ub3RpZmljYXRpb24YICABKAsyOi5jbGllbn'
'Nlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0KCmdvb2ds' 'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIY'
'ZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGAEgASgDUg' 'cGFzc3dvcmRsZXNzTm90aWZpY2F0aW9uGmoKC1RleHRNZXNzYWdlEhcKB3VzZXJfaWQYASABKA'
'Z1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFwoVR2V0QWRkQWNjb3VudHNJbnZpdGVzGhUK' 'NSBnVzZXJJZBISCgRib2R5GAMgASgMUgRib2R5EiAKCXB1c2hfZGF0YRgEIAEoDEgAUghwdXNo'
'E0dldEN1cnJlbnRQbGFuSW5mb3MaLwoUUmVtb3ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZB' 'RGF0YYgBAUIMCgpfcHVzaF9kYXRhGi8KEUdldFVzZXJCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA'
'gBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1' 'EgASgJUgh1c2VybmFtZRosCg5DaGFuZ2VVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNl'
'c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlck' 'cm5hbWUaNQoUVXBkYXRlR29vZ2xlRmNtVG9rZW4SHQoKZ29vZ2xlX2ZjbRgBIAEoCVIJZ29vZ2'
'lkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQc2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2ln' 'xlRmNtGiYKC0dldFVzZXJCeUlkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoTChFHZXRBdmFp'
'bmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleRgCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ2' 'bGFibGVQbGFucxoXChVHZXRBZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5Jbm'
'5lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93' 'ZvcxovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoS'
'bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG' 'R2V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZW'
'9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVh' 'RQcmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25l'
'c29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcH' 'ZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaW'
'JvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMg' 'duZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1'
'ASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudB' 'cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bm'
'osChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMAoNU2V0TG9n' 'xvYWRfdG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRl'
'aW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpEZXByZWNhdGVkQh' 'ZF91c2VyX2lkGAEgASgDUg5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbh'
'EKD0FwcGxpY2F0aW9uRGF0YQ=='); 'pxCgtJUEFQdXJjaGFzZRIdCgpwcm9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNl'
'GAIgASgJUgZzb3VyY2USKwoRdmVyaWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbk'
'RhdGEaDwoNSVBBRm9yY2VDaGVjaxoPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxV'
'c2VyEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBowCg1TZXRMb2dpblRva2VuEh8KC2xvZ2luX3'
'Rva2VuGAEgASgMUgpsb2dpblRva2VuGgwKCkRlcHJlY2F0ZWQajgEKHFJlZ2lzdGVyUGFzc3dv'
'cmRMZXNzUmVjb3ZlcnkSLgoSZW5jcnlwdGVkU2VydmVyS2V5GAEgASgMUhJlbmNyeXB0ZWRTZX'
'J2ZXJLZXkSKwoOcGluVW5sb2NrVG9rZW4YAiABKAxIAFIOcGluVW5sb2NrVG9rZW6IAQFCEQoP'
'X3BpblVubG9ja1Rva2VuGnAKGFBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYX'
'Rpb25faWQYASABKAlSDm5vdGlmaWNhdGlvbklkEisKEWVuY3J5cHRlZF9tZXNzYWdlGAIgASgM'
'UhBlbmNyeXB0ZWRNZXNzYWdlQhEKD0FwcGxpY2F0aW9uRGF0YQ==');
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response$json = { const Response$json = {

View file

@ -93,6 +93,10 @@ class ErrorCode extends $pb.ProtobufEnum {
ErrorCode._(1035, _omitEnumNames ? '' : 'UserIsNotInFreePlan'); ErrorCode._(1035, _omitEnumNames ? '' : 'UserIsNotInFreePlan');
static const ErrorCode ForegroundSessionConnected = static const ErrorCode ForegroundSessionConnected =
ErrorCode._(1036, _omitEnumNames ? '' : 'ForegroundSessionConnected'); ErrorCode._(1036, _omitEnumNames ? '' : 'ForegroundSessionConnected');
static const ErrorCode NoRecoveryData =
ErrorCode._(1037, _omitEnumNames ? '' : 'NoRecoveryData');
static const ErrorCode InvalidRecoveryData =
ErrorCode._(1038, _omitEnumNames ? '' : 'InvalidRecoveryData');
static const $core.List<ErrorCode> values = <ErrorCode>[ static const $core.List<ErrorCode> values = <ErrorCode>[
Unknown, Unknown,
@ -134,6 +138,8 @@ class ErrorCode extends $pb.ProtobufEnum {
IPAPaymentExpired, IPAPaymentExpired,
UserIsNotInFreePlan, UserIsNotInFreePlan,
ForegroundSessionConnected, ForegroundSessionConnected,
NoRecoveryData,
InvalidRecoveryData,
]; ];
static final $core.Map<$core.int, ErrorCode> _byValue = static final $core.Map<$core.int, ErrorCode> _byValue =

View file

@ -58,6 +58,8 @@ const ErrorCode$json = {
{'1': 'IPAPaymentExpired', '2': 1034}, {'1': 'IPAPaymentExpired', '2': 1034},
{'1': 'UserIsNotInFreePlan', '2': 1035}, {'1': 'UserIsNotInFreePlan', '2': 1035},
{'1': 'ForegroundSessionConnected', '2': 1036}, {'1': 'ForegroundSessionConnected', '2': 1036},
{'1': 'NoRecoveryData', '2': 1037},
{'1': 'InvalidRecoveryData', '2': 1038},
], ],
}; };
@ -80,4 +82,5 @@ final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode(
'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh' 'EIUIEhcKEkFwcFZlcnNpb25PdXRkYXRlZBCGCBIYChNOZXdEZXZpY2VSZWdpc3RlcmVkEIcIEh'
'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ' 'cKEkludmFsaWRQcm9vZk9mV29yaxCICBIZChRSZWdpc3RyYXRpb25EaXNhYmxlZBCJCBIWChFJ'
'UEFQYXltZW50RXhwaXJlZBCKCBIYChNVc2VySXNOb3RJbkZyZWVQbGFuEIsIEh8KGkZvcmVncm' 'UEFQYXltZW50RXhwaXJlZBCKCBIYChNVc2VySXNOb3RJbkZyZWVQbGFuEIsIEh8KGkZvcmVncm'
'91bmRTZXNzaW9uQ29ubmVjdGVkEIwI'); '91bmRTZXNzaW9uQ29ubmVjdGVkEIwIEhMKDk5vUmVjb3ZlcnlEYXRhEI0IEhgKE0ludmFsaWRS'
'ZWNvdmVyeURhdGEQjgg=');

View file

@ -1505,6 +1505,136 @@ class Response_ProofOfWork extends $pb.GeneratedMessage {
void clearDifficulty() => $_clearField(2); void clearDifficulty() => $_clearField(2);
} }
class Response_PasswordlessNotificationMessage extends $pb.GeneratedMessage {
factory Response_PasswordlessNotificationMessage({
$fixnum.Int64? id,
$core.List<$core.int>? encryptedMessage,
}) {
final result = create();
if (id != null) result.id = id;
if (encryptedMessage != null) result.encryptedMessage = encryptedMessage;
return result;
}
Response_PasswordlessNotificationMessage._();
factory Response_PasswordlessNotificationMessage.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_PasswordlessNotificationMessage.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.PasswordlessNotificationMessage',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'id')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'encryptedMessage', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PasswordlessNotificationMessage clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PasswordlessNotificationMessage copyWith(
void Function(Response_PasswordlessNotificationMessage) updates) =>
super.copyWith((message) =>
updates(message as Response_PasswordlessNotificationMessage))
as Response_PasswordlessNotificationMessage;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_PasswordlessNotificationMessage create() =>
Response_PasswordlessNotificationMessage._();
@$core.override
Response_PasswordlessNotificationMessage createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_PasswordlessNotificationMessage getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
Response_PasswordlessNotificationMessage>(create);
static Response_PasswordlessNotificationMessage? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get id => $_getI64(0);
@$pb.TagNumber(1)
set id($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasId() => $_has(0);
@$pb.TagNumber(1)
void clearId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get encryptedMessage => $_getN(1);
@$pb.TagNumber(2)
set encryptedMessage($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasEncryptedMessage() => $_has(1);
@$pb.TagNumber(2)
void clearEncryptedMessage() => $_clearField(2);
}
class Response_PasswordlessNotificationMessages extends $pb.GeneratedMessage {
factory Response_PasswordlessNotificationMessages({
$core.Iterable<Response_PasswordlessNotificationMessage>? messages,
}) {
final result = create();
if (messages != null) result.messages.addAll(messages);
return result;
}
Response_PasswordlessNotificationMessages._();
factory Response_PasswordlessNotificationMessages.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_PasswordlessNotificationMessages.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.PasswordlessNotificationMessages',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..pPM<Response_PasswordlessNotificationMessage>(
1, _omitFieldNames ? '' : 'messages',
subBuilder: Response_PasswordlessNotificationMessage.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PasswordlessNotificationMessages clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PasswordlessNotificationMessages copyWith(
void Function(Response_PasswordlessNotificationMessages) updates) =>
super.copyWith((message) =>
updates(message as Response_PasswordlessNotificationMessages))
as Response_PasswordlessNotificationMessages;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_PasswordlessNotificationMessages create() =>
Response_PasswordlessNotificationMessages._();
@$core.override
Response_PasswordlessNotificationMessages createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_PasswordlessNotificationMessages getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
Response_PasswordlessNotificationMessages>(create);
static Response_PasswordlessNotificationMessages? _defaultInstance;
@$pb.TagNumber(1)
$pb.PbList<Response_PasswordlessNotificationMessage> get messages =>
$_getList(0);
}
enum Response_Ok_Ok { enum Response_Ok_Ok {
none, none,
userid, userid,
@ -1521,6 +1651,8 @@ enum Response_Ok_Ok {
downloadtokens, downloadtokens,
signedprekey, signedprekey,
proofOfWork, proofOfWork,
passwordlessRecoveryServerKey,
passwordlessNotificationMessages,
notSet notSet
} }
@ -1541,6 +1673,8 @@ class Response_Ok extends $pb.GeneratedMessage {
Response_DownloadTokens? downloadtokens, Response_DownloadTokens? downloadtokens,
Response_SignedPreKey? signedprekey, Response_SignedPreKey? signedprekey,
Response_ProofOfWork? proofOfWork, Response_ProofOfWork? proofOfWork,
$core.List<$core.int>? passwordlessRecoveryServerKey,
Response_PasswordlessNotificationMessages? passwordlessNotificationMessages,
}) { }) {
final result = create(); final result = create();
if (none != null) result.none = none; if (none != null) result.none = none;
@ -1559,6 +1693,11 @@ class Response_Ok extends $pb.GeneratedMessage {
if (downloadtokens != null) result.downloadtokens = downloadtokens; if (downloadtokens != null) result.downloadtokens = downloadtokens;
if (signedprekey != null) result.signedprekey = signedprekey; if (signedprekey != null) result.signedprekey = signedprekey;
if (proofOfWork != null) result.proofOfWork = proofOfWork; if (proofOfWork != null) result.proofOfWork = proofOfWork;
if (passwordlessRecoveryServerKey != null)
result.passwordlessRecoveryServerKey = passwordlessRecoveryServerKey;
if (passwordlessNotificationMessages != null)
result.passwordlessNotificationMessages =
passwordlessNotificationMessages;
return result; return result;
} }
@ -1587,6 +1726,8 @@ class Response_Ok extends $pb.GeneratedMessage {
13: Response_Ok_Ok.downloadtokens, 13: Response_Ok_Ok.downloadtokens,
14: Response_Ok_Ok.signedprekey, 14: Response_Ok_Ok.signedprekey,
15: Response_Ok_Ok.proofOfWork, 15: Response_Ok_Ok.proofOfWork,
16: Response_Ok_Ok.passwordlessRecoveryServerKey,
17: Response_Ok_Ok.passwordlessNotificationMessages,
0: Response_Ok_Ok.notSet 0: Response_Ok_Ok.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -1594,7 +1735,7 @@ class Response_Ok extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create) createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17])
..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None') ..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None')
..aInt64(2, _omitFieldNames ? '' : 'userid') ..aInt64(2, _omitFieldNames ? '' : 'userid')
..a<$core.List<$core.int>>( ..a<$core.List<$core.int>>(
@ -1624,6 +1765,13 @@ class Response_Ok extends $pb.GeneratedMessage {
subBuilder: Response_SignedPreKey.create) subBuilder: Response_SignedPreKey.create)
..aOM<Response_ProofOfWork>(15, _omitFieldNames ? '' : 'proofOfWork', ..aOM<Response_ProofOfWork>(15, _omitFieldNames ? '' : 'proofOfWork',
protoName: 'proofOfWork', subBuilder: Response_ProofOfWork.create) protoName: 'proofOfWork', subBuilder: Response_ProofOfWork.create)
..a<$core.List<$core.int>>(
16,
_omitFieldNames ? '' : 'passwordlessRecoveryServerKey',
$pb.PbFieldType.OY)
..aOM<Response_PasswordlessNotificationMessages>(
17, _omitFieldNames ? '' : 'passwordlessNotificationMessages',
subBuilder: Response_PasswordlessNotificationMessages.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -1660,6 +1808,8 @@ class Response_Ok extends $pb.GeneratedMessage {
@$pb.TagNumber(13) @$pb.TagNumber(13)
@$pb.TagNumber(14) @$pb.TagNumber(14)
@$pb.TagNumber(15) @$pb.TagNumber(15)
@$pb.TagNumber(16)
@$pb.TagNumber(17)
Response_Ok_Ok whichOk() => _Response_Ok_OkByTag[$_whichOneof(0)]!; Response_Ok_Ok whichOk() => _Response_Ok_OkByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1) @$pb.TagNumber(1)
@$pb.TagNumber(2) @$pb.TagNumber(2)
@ -1676,6 +1826,8 @@ class Response_Ok extends $pb.GeneratedMessage {
@$pb.TagNumber(13) @$pb.TagNumber(13)
@$pb.TagNumber(14) @$pb.TagNumber(14)
@$pb.TagNumber(15) @$pb.TagNumber(15)
@$pb.TagNumber(16)
@$pb.TagNumber(17)
void clearOk() => $_clearField($_whichOneof(0)); void clearOk() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -1835,6 +1987,31 @@ class Response_Ok extends $pb.GeneratedMessage {
void clearProofOfWork() => $_clearField(15); void clearProofOfWork() => $_clearField(15);
@$pb.TagNumber(15) @$pb.TagNumber(15)
Response_ProofOfWork ensureProofOfWork() => $_ensure(14); Response_ProofOfWork ensureProofOfWork() => $_ensure(14);
@$pb.TagNumber(16)
$core.List<$core.int> get passwordlessRecoveryServerKey => $_getN(15);
@$pb.TagNumber(16)
set passwordlessRecoveryServerKey($core.List<$core.int> value) =>
$_setBytes(15, value);
@$pb.TagNumber(16)
$core.bool hasPasswordlessRecoveryServerKey() => $_has(15);
@$pb.TagNumber(16)
void clearPasswordlessRecoveryServerKey() => $_clearField(16);
@$pb.TagNumber(17)
Response_PasswordlessNotificationMessages
get passwordlessNotificationMessages => $_getN(16);
@$pb.TagNumber(17)
set passwordlessNotificationMessages(
Response_PasswordlessNotificationMessages value) =>
$_setField(17, value);
@$pb.TagNumber(17)
$core.bool hasPasswordlessNotificationMessages() => $_has(16);
@$pb.TagNumber(17)
void clearPasswordlessNotificationMessages() => $_clearField(17);
@$pb.TagNumber(17)
Response_PasswordlessNotificationMessages
ensurePasswordlessNotificationMessages() => $_ensure(16);
} }
enum Response_Response { ok, error, notSet } enum Response_Response { ok, error, notSet }

View file

@ -176,6 +176,8 @@ const Response$json = {
Response_UploadToken$json, Response_UploadToken$json,
Response_DownloadTokens$json, Response_DownloadTokens$json,
Response_ProofOfWork$json, Response_ProofOfWork$json,
Response_PasswordlessNotificationMessage$json,
Response_PasswordlessNotificationMessages$json,
Response_Ok$json Response_Ok$json
], ],
'8': [ '8': [
@ -509,6 +511,36 @@ const Response_ProofOfWork$json = {
], ],
}; };
@$core.Deprecated('Use responseDescriptor instead')
const Response_PasswordlessNotificationMessage$json = {
'1': 'PasswordlessNotificationMessage',
'2': [
{'1': 'id', '3': 1, '4': 1, '5': 3, '10': 'id'},
{
'1': 'encrypted_message',
'3': 2,
'4': 1,
'5': 12,
'10': 'encryptedMessage'
},
],
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_PasswordlessNotificationMessages$json = {
'1': 'PasswordlessNotificationMessages',
'2': [
{
'1': 'messages',
'3': 1,
'4': 3,
'5': 11,
'6': '.server_to_client.Response.PasswordlessNotificationMessage',
'10': 'messages'
},
],
};
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response_Ok$json = { const Response_Ok$json = {
'1': 'Ok', '1': 'Ok',
@ -623,6 +655,23 @@ const Response_Ok$json = {
'9': 0, '9': 0,
'10': 'proofOfWork' '10': 'proofOfWork'
}, },
{
'1': 'passwordless_recovery_server_key',
'3': 16,
'4': 1,
'5': 12,
'9': 0,
'10': 'passwordlessRecoveryServerKey'
},
{
'1': 'passwordless_notification_messages',
'3': 17,
'4': 1,
'5': 11,
'6': '.server_to_client.Response.PasswordlessNotificationMessages',
'9': 0,
'10': 'passwordlessNotificationMessages'
},
], ],
'8': [ '8': [
{'1': 'Ok'}, {'1': 'Ok'},
@ -677,22 +726,29 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'cKD2Rvd25sb2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tl' 'cKD2Rvd25sb2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tl'
'bnMSJwoPZG93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV2' 'bnMSJwoPZG93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV2'
'9yaxIWCgZwcmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1' '9yaxIWCgZwcmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1'
'bHR5GtcHCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBnVzZX' 'bHR5Gl4KH1Bhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2USDgoCaWQYASABKANSAmlkEi'
'JpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9r' 'sKEWVuY3J5cHRlZF9tZXNzYWdlGAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGnoKIFBhc3N3b3Jk'
'ZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG' 'bGVzc05vdGlmaWNhdGlvbk1lc3NhZ2VzElYKCG1lc3NhZ2VzGAEgAygLMjouc2VydmVyX3RvX2'
'9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5V' 'NsaWVudC5SZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdl'
'c2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEkoKDG' 'cxqwCQoCT2sSFAoETm9uZRgBIAEoCEgAUgROb25lEhgKBnVzZXJpZBgCIAEoA0gAUgZ1c2VyaW'
'RlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuRGVwcmVjYXRl' 'QSJgoNYXV0aGNoYWxsZW5nZRgDIAEoDEgAUg1hdXRoY2hhbGxlbmdlEkoKC3VwbG9hZHRva2Vu'
'ZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2NsaW' 'GAQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5VcGxvYWRUb2tlbkgAUgt1cGxvYW'
'VudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSAB' 'R0b2tlbhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuVXNl'
'KAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW5iYW' 'ckRhdGFIAFIIdXNlcmRhdGESHgoJYXV0aHRva2VuGAYgASgMSABSCWF1dGh0b2tlbhJKCgxkZX'
'xsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNlSABS' 'ByZWNhdGVkXzcYByABKAsyJS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRlcHJlY2F0ZWRI'
'DHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGAsgASgLMiUuc2VydmVyX3RvX2NsaWVudC' 'AFILZGVwcmVjYXRlZDcSUAoNYXV0aGVudGljYXRlZBgIIAEoCzIoLnNlcnZlcl90b19jbGllbn'
'5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxMRJfChJhZGRhY2NvdW50c2ludml0' 'QuUmVzcG9uc2UuQXV0aGVudGljYXRlZEgAUg1hdXRoZW50aWNhdGVkEjgKBXBsYW5zGAkgASgL'
'ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aXRlc0' 'MiAuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuc0gAUgVwbGFucxJNCgxwbGFuYmFsbG'
'gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJf' 'FuY2UYCiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5CYWxsYW5jZUgAUgxw'
'dG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk0KDH' 'bGFuYmFsbGFuY2USTAoNZGVwcmVjYXRlZF8xMRgLIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUm'
'NpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVkUHJl' 'VzcG9uc2UuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTESXwoSYWRkYWNjb3VudHNpbnZpdGVz'
'S2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b19jbG' 'GAwgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRBY2NvdW50c0ludml0ZXNIAF'
'llbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmtCBAoCT2tCCgoIUmVzcG9u' 'ISYWRkYWNjb3VudHNpbnZpdGVzElMKDmRvd25sb2FkdG9rZW5zGA0gASgLMikuc2VydmVyX3Rv'
'c2U='); 'X2NsaWVudC5SZXNwb25zZS5Eb3dubG9hZFRva2Vuc0gAUg5kb3dubG9hZHRva2VucxJNCgxzaW'
'duZWRwcmVrZXkYDiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlNpZ25lZFByZUtl'
'eUgAUgxzaWduZWRwcmVrZXkSSgoLcHJvb2ZPZldvcmsYDyABKAsyJi5zZXJ2ZXJfdG9fY2xpZW'
'50LlJlc3BvbnNlLlByb29mT2ZXb3JrSABSC3Byb29mT2ZXb3JrEkkKIHBhc3N3b3JkbGVzc19y'
'ZWNvdmVyeV9zZXJ2ZXJfa2V5GBAgASgMSABSHXBhc3N3b3JkbGVzc1JlY292ZXJ5U2VydmVyS2'
'V5EosBCiJwYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uX21lc3NhZ2VzGBEgASgLMjsuc2VydmVy'
'X3RvX2NsaWVudC5SZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlc0gAUi'
'BwYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlc0IECgJPa0IKCghSZXNwb25zZQ==');

View file

@ -368,10 +368,12 @@ class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
factory EncryptedContent_GroupCreate({ factory EncryptedContent_GroupCreate({
$core.List<$core.int>? stateKey, $core.List<$core.int>? stateKey,
$core.List<$core.int>? groupPublicKey, $core.List<$core.int>? groupPublicKey,
$core.String? groupName,
}) { }) {
final result = create(); final result = create();
if (stateKey != null) result.stateKey = stateKey; if (stateKey != null) result.stateKey = stateKey;
if (groupPublicKey != null) result.groupPublicKey = groupPublicKey; if (groupPublicKey != null) result.groupPublicKey = groupPublicKey;
if (groupName != null) result.groupName = groupName;
return result; return result;
} }
@ -391,6 +393,7 @@ class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
3, _omitFieldNames ? '' : 'stateKey', $pb.PbFieldType.OY) 3, _omitFieldNames ? '' : 'stateKey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>( ..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY) 4, _omitFieldNames ? '' : 'groupPublicKey', $pb.PbFieldType.OY)
..aOS(5, _omitFieldNames ? '' : 'groupName')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -433,6 +436,15 @@ class EncryptedContent_GroupCreate extends $pb.GeneratedMessage {
$core.bool hasGroupPublicKey() => $_has(1); $core.bool hasGroupPublicKey() => $_has(1);
@$pb.TagNumber(4) @$pb.TagNumber(4)
void clearGroupPublicKey() => $_clearField(4); void clearGroupPublicKey() => $_clearField(4);
@$pb.TagNumber(5)
$core.String get groupName => $_getSZ(2);
@$pb.TagNumber(5)
set groupName($core.String value) => $_setString(2, value);
@$pb.TagNumber(5)
$core.bool hasGroupName() => $_has(2);
@$pb.TagNumber(5)
void clearGroupName() => $_clearField(5);
} }
class EncryptedContent_GroupJoin extends $pb.GeneratedMessage { class EncryptedContent_GroupJoin extends $pb.GeneratedMessage {
@ -1827,6 +1839,154 @@ class EncryptedContent_KeyVerificationProof extends $pb.GeneratedMessage {
void clearCalculatedMac() => $_clearField(1); void clearCalculatedMac() => $_clearField(1);
} }
class EncryptedContent_PasswordLessRecovery extends $pb.GeneratedMessage {
factory EncryptedContent_PasswordLessRecovery({
$core.List<$core.int>? recoverySecretShare,
$core.bool? delete,
$fixnum.Int64? threshold,
}) {
final result = create();
if (recoverySecretShare != null)
result.recoverySecretShare = recoverySecretShare;
if (delete != null) result.delete = delete;
if (threshold != null) result.threshold = threshold;
return result;
}
EncryptedContent_PasswordLessRecovery._();
factory EncryptedContent_PasswordLessRecovery.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EncryptedContent_PasswordLessRecovery.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EncryptedContent.PasswordLessRecovery',
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'recoverySecretShare', $pb.PbFieldType.OY,
protoName: 'recoverySecretShare')
..aOB(2, _omitFieldNames ? '' : 'delete')
..aInt64(3, _omitFieldNames ? '' : 'threshold')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_PasswordLessRecovery clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_PasswordLessRecovery copyWith(
void Function(EncryptedContent_PasswordLessRecovery) updates) =>
super.copyWith((message) =>
updates(message as EncryptedContent_PasswordLessRecovery))
as EncryptedContent_PasswordLessRecovery;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EncryptedContent_PasswordLessRecovery create() =>
EncryptedContent_PasswordLessRecovery._();
@$core.override
EncryptedContent_PasswordLessRecovery createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static EncryptedContent_PasswordLessRecovery getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
EncryptedContent_PasswordLessRecovery>(create);
static EncryptedContent_PasswordLessRecovery? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get recoverySecretShare => $_getN(0);
@$pb.TagNumber(1)
set recoverySecretShare($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasRecoverySecretShare() => $_has(0);
@$pb.TagNumber(1)
void clearRecoverySecretShare() => $_clearField(1);
@$pb.TagNumber(2)
$core.bool get delete => $_getBF(1);
@$pb.TagNumber(2)
set delete($core.bool value) => $_setBool(1, value);
@$pb.TagNumber(2)
$core.bool hasDelete() => $_has(1);
@$pb.TagNumber(2)
void clearDelete() => $_clearField(2);
@$pb.TagNumber(3)
$fixnum.Int64 get threshold => $_getI64(2);
@$pb.TagNumber(3)
set threshold($fixnum.Int64 value) => $_setInt64(2, value);
@$pb.TagNumber(3)
$core.bool hasThreshold() => $_has(2);
@$pb.TagNumber(3)
void clearThreshold() => $_clearField(3);
}
class EncryptedContent_PasswordLessRecoveryHeartbeat
extends $pb.GeneratedMessage {
factory EncryptedContent_PasswordLessRecoveryHeartbeat({
$core.List<$core.int>? hash,
}) {
final result = create();
if (hash != null) result.hash = hash;
return result;
}
EncryptedContent_PasswordLessRecoveryHeartbeat._();
factory EncryptedContent_PasswordLessRecoveryHeartbeat.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EncryptedContent_PasswordLessRecoveryHeartbeat.fromJson(
$core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EncryptedContent.PasswordLessRecoveryHeartbeat',
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'hash', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_PasswordLessRecoveryHeartbeat clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedContent_PasswordLessRecoveryHeartbeat copyWith(
void Function(EncryptedContent_PasswordLessRecoveryHeartbeat)
updates) =>
super.copyWith((message) => updates(
message as EncryptedContent_PasswordLessRecoveryHeartbeat))
as EncryptedContent_PasswordLessRecoveryHeartbeat;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EncryptedContent_PasswordLessRecoveryHeartbeat create() =>
EncryptedContent_PasswordLessRecoveryHeartbeat._();
@$core.override
EncryptedContent_PasswordLessRecoveryHeartbeat createEmptyInstance() =>
create();
@$core.pragma('dart2js:noInline')
static EncryptedContent_PasswordLessRecoveryHeartbeat getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<
EncryptedContent_PasswordLessRecoveryHeartbeat>(create);
static EncryptedContent_PasswordLessRecoveryHeartbeat? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get hash => $_getN(0);
@$pb.TagNumber(1)
set hash($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasHash() => $_has(0);
@$pb.TagNumber(1)
void clearHash() => $_clearField(1);
}
class EncryptedContent extends $pb.GeneratedMessage { class EncryptedContent extends $pb.GeneratedMessage {
factory EncryptedContent({ factory EncryptedContent({
$core.String? groupId, $core.String? groupId,
@ -1852,6 +2012,10 @@ class EncryptedContent extends $pb.GeneratedMessage {
EncryptedContent_UserDiscoveryRequest? userDiscoveryRequest, EncryptedContent_UserDiscoveryRequest? userDiscoveryRequest,
EncryptedContent_UserDiscoveryUpdate? userDiscoveryUpdate, EncryptedContent_UserDiscoveryUpdate? userDiscoveryUpdate,
EncryptedContent_KeyVerificationProof? keyVerificationProof, EncryptedContent_KeyVerificationProof? keyVerificationProof,
$core.bool? askForFriendPromotions,
EncryptedContent_PasswordLessRecovery? passwordlessRecovery,
EncryptedContent_PasswordLessRecoveryHeartbeat?
passwordlessRecoveryHeartbeat,
}) { }) {
final result = create(); final result = create();
if (groupId != null) result.groupId = groupId; if (groupId != null) result.groupId = groupId;
@ -1884,6 +2048,12 @@ class EncryptedContent extends $pb.GeneratedMessage {
result.userDiscoveryUpdate = userDiscoveryUpdate; result.userDiscoveryUpdate = userDiscoveryUpdate;
if (keyVerificationProof != null) if (keyVerificationProof != null)
result.keyVerificationProof = keyVerificationProof; result.keyVerificationProof = keyVerificationProof;
if (askForFriendPromotions != null)
result.askForFriendPromotions = askForFriendPromotions;
if (passwordlessRecovery != null)
result.passwordlessRecovery = passwordlessRecovery;
if (passwordlessRecoveryHeartbeat != null)
result.passwordlessRecoveryHeartbeat = passwordlessRecoveryHeartbeat;
return result; return result;
} }
@ -1955,6 +2125,13 @@ class EncryptedContent extends $pb.GeneratedMessage {
..aOM<EncryptedContent_KeyVerificationProof>( ..aOM<EncryptedContent_KeyVerificationProof>(
24, _omitFieldNames ? '' : 'keyVerificationProof', 24, _omitFieldNames ? '' : 'keyVerificationProof',
subBuilder: EncryptedContent_KeyVerificationProof.create) subBuilder: EncryptedContent_KeyVerificationProof.create)
..aOB(25, _omitFieldNames ? '' : 'askForFriendPromotions')
..aOM<EncryptedContent_PasswordLessRecovery>(
26, _omitFieldNames ? '' : 'passwordlessRecovery',
subBuilder: EncryptedContent_PasswordLessRecovery.create)
..aOM<EncryptedContent_PasswordLessRecoveryHeartbeat>(
27, _omitFieldNames ? '' : 'passwordlessRecoveryHeartbeat',
subBuilder: EncryptedContent_PasswordLessRecoveryHeartbeat.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -2238,6 +2415,43 @@ class EncryptedContent extends $pb.GeneratedMessage {
@$pb.TagNumber(24) @$pb.TagNumber(24)
EncryptedContent_KeyVerificationProof ensureKeyVerificationProof() => EncryptedContent_KeyVerificationProof ensureKeyVerificationProof() =>
$_ensure(22); $_ensure(22);
@$pb.TagNumber(25)
$core.bool get askForFriendPromotions => $_getBF(23);
@$pb.TagNumber(25)
set askForFriendPromotions($core.bool value) => $_setBool(23, value);
@$pb.TagNumber(25)
$core.bool hasAskForFriendPromotions() => $_has(23);
@$pb.TagNumber(25)
void clearAskForFriendPromotions() => $_clearField(25);
@$pb.TagNumber(26)
EncryptedContent_PasswordLessRecovery get passwordlessRecovery => $_getN(24);
@$pb.TagNumber(26)
set passwordlessRecovery(EncryptedContent_PasswordLessRecovery value) =>
$_setField(26, value);
@$pb.TagNumber(26)
$core.bool hasPasswordlessRecovery() => $_has(24);
@$pb.TagNumber(26)
void clearPasswordlessRecovery() => $_clearField(26);
@$pb.TagNumber(26)
EncryptedContent_PasswordLessRecovery ensurePasswordlessRecovery() =>
$_ensure(24);
@$pb.TagNumber(27)
EncryptedContent_PasswordLessRecoveryHeartbeat
get passwordlessRecoveryHeartbeat => $_getN(25);
@$pb.TagNumber(27)
set passwordlessRecoveryHeartbeat(
EncryptedContent_PasswordLessRecoveryHeartbeat value) =>
$_setField(27, value);
@$pb.TagNumber(27)
$core.bool hasPasswordlessRecoveryHeartbeat() => $_has(25);
@$pb.TagNumber(27)
void clearPasswordlessRecoveryHeartbeat() => $_clearField(27);
@$pb.TagNumber(27)
EncryptedContent_PasswordLessRecoveryHeartbeat
ensurePasswordlessRecoveryHeartbeat() => $_ensure(25);
} }
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =

View file

@ -79,16 +79,20 @@ class EncryptedContent_ErrorMessages_Type extends $pb.ProtobufEnum {
static const EncryptedContent_ErrorMessages_Type SESSION_OUT_OF_SYNC = static const EncryptedContent_ErrorMessages_Type SESSION_OUT_OF_SYNC =
EncryptedContent_ErrorMessages_Type._( EncryptedContent_ErrorMessages_Type._(
3, _omitEnumNames ? '' : 'SESSION_OUT_OF_SYNC'); 3, _omitEnumNames ? '' : 'SESSION_OUT_OF_SYNC');
static const EncryptedContent_ErrorMessages_Type
GROUP_NOT_FOUND_OR_NOT_A_MEMBER = EncryptedContent_ErrorMessages_Type._(
4, _omitEnumNames ? '' : 'GROUP_NOT_FOUND_OR_NOT_A_MEMBER');
static const $core.List<EncryptedContent_ErrorMessages_Type> values = static const $core.List<EncryptedContent_ErrorMessages_Type> values =
<EncryptedContent_ErrorMessages_Type>[ <EncryptedContent_ErrorMessages_Type>[
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD, ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
UNKNOWN_MESSAGE_TYPE, UNKNOWN_MESSAGE_TYPE,
SESSION_OUT_OF_SYNC, SESSION_OUT_OF_SYNC,
GROUP_NOT_FOUND_OR_NOT_A_MEMBER,
]; ];
static final $core.List<EncryptedContent_ErrorMessages_Type?> _byValue = static final $core.List<EncryptedContent_ErrorMessages_Type?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 3); $pb.ProtobufEnum.$_initByValueList(values, 4);
static EncryptedContent_ErrorMessages_Type? valueOf($core.int value) => static EncryptedContent_ErrorMessages_Type? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value]; value < 0 || value >= _byValue.length ? null : _byValue[value];

View file

@ -186,13 +186,22 @@ const EncryptedContent$json = {
'10': 'senderUserDiscoveryVersion', '10': 'senderUserDiscoveryVersion',
'17': true '17': true
}, },
{
'1': 'ask_for_friend_promotions',
'3': 25,
'4': 1,
'5': 8,
'9': 4,
'10': 'askForFriendPromotions',
'17': true
},
{ {
'1': 'message_update', '1': 'message_update',
'3': 5, '3': 5,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.MessageUpdate', '6': '.EncryptedContent.MessageUpdate',
'9': 4, '9': 5,
'10': 'messageUpdate', '10': 'messageUpdate',
'17': true '17': true
}, },
@ -202,7 +211,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.Media', '6': '.EncryptedContent.Media',
'9': 5, '9': 6,
'10': 'media', '10': 'media',
'17': true '17': true
}, },
@ -212,7 +221,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.MediaUpdate', '6': '.EncryptedContent.MediaUpdate',
'9': 6, '9': 7,
'10': 'mediaUpdate', '10': 'mediaUpdate',
'17': true '17': true
}, },
@ -222,7 +231,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.ContactUpdate', '6': '.EncryptedContent.ContactUpdate',
'9': 7, '9': 8,
'10': 'contactUpdate', '10': 'contactUpdate',
'17': true '17': true
}, },
@ -232,7 +241,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.ContactRequest', '6': '.EncryptedContent.ContactRequest',
'9': 8, '9': 9,
'10': 'contactRequest', '10': 'contactRequest',
'17': true '17': true
}, },
@ -242,7 +251,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.FlameSync', '6': '.EncryptedContent.FlameSync',
'9': 9, '9': 10,
'10': 'flameSync', '10': 'flameSync',
'17': true '17': true
}, },
@ -252,7 +261,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.PushKeys', '6': '.EncryptedContent.PushKeys',
'9': 10, '9': 11,
'10': 'pushKeys', '10': 'pushKeys',
'17': true '17': true
}, },
@ -262,7 +271,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.Reaction', '6': '.EncryptedContent.Reaction',
'9': 11, '9': 12,
'10': 'reaction', '10': 'reaction',
'17': true '17': true
}, },
@ -272,7 +281,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.TextMessage', '6': '.EncryptedContent.TextMessage',
'9': 12, '9': 13,
'10': 'textMessage', '10': 'textMessage',
'17': true '17': true
}, },
@ -282,7 +291,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.GroupCreate', '6': '.EncryptedContent.GroupCreate',
'9': 13, '9': 14,
'10': 'groupCreate', '10': 'groupCreate',
'17': true '17': true
}, },
@ -292,7 +301,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.GroupJoin', '6': '.EncryptedContent.GroupJoin',
'9': 14, '9': 15,
'10': 'groupJoin', '10': 'groupJoin',
'17': true '17': true
}, },
@ -302,7 +311,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.GroupUpdate', '6': '.EncryptedContent.GroupUpdate',
'9': 15, '9': 16,
'10': 'groupUpdate', '10': 'groupUpdate',
'17': true '17': true
}, },
@ -312,7 +321,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.ResendGroupPublicKey', '6': '.EncryptedContent.ResendGroupPublicKey',
'9': 16, '9': 17,
'10': 'resendGroupPublicKey', '10': 'resendGroupPublicKey',
'17': true '17': true
}, },
@ -322,7 +331,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.ErrorMessages', '6': '.EncryptedContent.ErrorMessages',
'9': 17, '9': 18,
'10': 'errorMessages', '10': 'errorMessages',
'17': true '17': true
}, },
@ -332,7 +341,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.AdditionalDataMessage', '6': '.EncryptedContent.AdditionalDataMessage',
'9': 18, '9': 19,
'10': 'additionalDataMessage', '10': 'additionalDataMessage',
'17': true '17': true
}, },
@ -342,7 +351,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.TypingIndicator', '6': '.EncryptedContent.TypingIndicator',
'9': 19, '9': 20,
'10': 'typingIndicator', '10': 'typingIndicator',
'17': true '17': true
}, },
@ -352,7 +361,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.UserDiscoveryRequest', '6': '.EncryptedContent.UserDiscoveryRequest',
'9': 20, '9': 21,
'10': 'userDiscoveryRequest', '10': 'userDiscoveryRequest',
'17': true '17': true
}, },
@ -362,7 +371,7 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.UserDiscoveryUpdate', '6': '.EncryptedContent.UserDiscoveryUpdate',
'9': 21, '9': 22,
'10': 'userDiscoveryUpdate', '10': 'userDiscoveryUpdate',
'17': true '17': true
}, },
@ -372,10 +381,30 @@ const EncryptedContent$json = {
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.EncryptedContent.KeyVerificationProof', '6': '.EncryptedContent.KeyVerificationProof',
'9': 22, '9': 23,
'10': 'keyVerificationProof', '10': 'keyVerificationProof',
'17': true '17': true
}, },
{
'1': 'passwordless_recovery',
'3': 26,
'4': 1,
'5': 11,
'6': '.EncryptedContent.PasswordLessRecovery',
'9': 24,
'10': 'passwordlessRecovery',
'17': true
},
{
'1': 'passwordless_recovery_heartbeat',
'3': 27,
'4': 1,
'5': 11,
'6': '.EncryptedContent.PasswordLessRecoveryHeartbeat',
'9': 25,
'10': 'passwordlessRecoveryHeartbeat',
'17': true
},
], ],
'3': [ '3': [
EncryptedContent_ErrorMessages$json, EncryptedContent_ErrorMessages$json,
@ -396,13 +425,16 @@ const EncryptedContent$json = {
EncryptedContent_TypingIndicator$json, EncryptedContent_TypingIndicator$json,
EncryptedContent_UserDiscoveryRequest$json, EncryptedContent_UserDiscoveryRequest$json,
EncryptedContent_UserDiscoveryUpdate$json, EncryptedContent_UserDiscoveryUpdate$json,
EncryptedContent_KeyVerificationProof$json EncryptedContent_KeyVerificationProof$json,
EncryptedContent_PasswordLessRecovery$json,
EncryptedContent_PasswordLessRecoveryHeartbeat$json
], ],
'8': [ '8': [
{'1': '_group_id'}, {'1': '_group_id'},
{'1': '_is_direct_chat'}, {'1': '_is_direct_chat'},
{'1': '_sender_profile_counter'}, {'1': '_sender_profile_counter'},
{'1': '_sender_user_discovery_version'}, {'1': '_sender_user_discovery_version'},
{'1': '_ask_for_friend_promotions'},
{'1': '_message_update'}, {'1': '_message_update'},
{'1': '_media'}, {'1': '_media'},
{'1': '_media_update'}, {'1': '_media_update'},
@ -422,6 +454,8 @@ const EncryptedContent$json = {
{'1': '_user_discovery_request'}, {'1': '_user_discovery_request'},
{'1': '_user_discovery_update'}, {'1': '_user_discovery_update'},
{'1': '_key_verification_proof'}, {'1': '_key_verification_proof'},
{'1': '_passwordless_recovery'},
{'1': '_passwordless_recovery_heartbeat'},
], ],
}; };
@ -455,6 +489,7 @@ const EncryptedContent_ErrorMessages_Type$json = {
{'1': 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD', '2': 0}, {'1': 'ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD', '2': 0},
{'1': 'UNKNOWN_MESSAGE_TYPE', '2': 2}, {'1': 'UNKNOWN_MESSAGE_TYPE', '2': 2},
{'1': 'SESSION_OUT_OF_SYNC', '2': 3}, {'1': 'SESSION_OUT_OF_SYNC', '2': 3},
{'1': 'GROUP_NOT_FOUND_OR_NOT_A_MEMBER', '2': 4},
], ],
}; };
@ -464,6 +499,18 @@ const EncryptedContent_GroupCreate$json = {
'2': [ '2': [
{'1': 'state_key', '3': 3, '4': 1, '5': 12, '10': 'stateKey'}, {'1': 'state_key', '3': 3, '4': 1, '5': 12, '10': 'stateKey'},
{'1': 'group_public_key', '3': 4, '4': 1, '5': 12, '10': 'groupPublicKey'}, {'1': 'group_public_key', '3': 4, '4': 1, '5': 12, '10': 'groupPublicKey'},
{
'1': 'group_name',
'3': 5,
'4': 1,
'5': 9,
'9': 0,
'10': 'groupName',
'17': true
},
],
'8': [
{'1': '_group_name'},
], ],
}; };
@ -932,109 +979,150 @@ const EncryptedContent_KeyVerificationProof$json = {
], ],
}; };
@$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_PasswordLessRecovery$json = {
'1': 'PasswordLessRecovery',
'2': [
{
'1': 'recoverySecretShare',
'3': 1,
'4': 1,
'5': 12,
'9': 0,
'10': 'recoverySecretShare',
'17': true
},
{'1': 'delete', '3': 2, '4': 1, '5': 8, '10': 'delete'},
{'1': 'threshold', '3': 3, '4': 1, '5': 3, '10': 'threshold'},
],
'8': [
{'1': '_recoverySecretShare'},
],
};
@$core.Deprecated('Use encryptedContentDescriptor instead')
const EncryptedContent_PasswordLessRecoveryHeartbeat$json = {
'1': 'PasswordLessRecoveryHeartbeat',
'2': [
{'1': 'hash', '3': 1, '4': 1, '5': 12, '10': 'hash'},
],
};
/// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `EncryptedContent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
'ChBFbmNyeXB0ZWRDb250ZW50Eh4KCGdyb3VwX2lkGAIgASgJSABSB2dyb3VwSWSIAQESKQoOaX' 'ChBFbmNyeXB0ZWRDb250ZW50Eh4KCGdyb3VwX2lkGAIgASgJSABSB2dyb3VwSWSIAQESKQoOaX'
'NfZGlyZWN0X2NoYXQYAyABKAhIAVIMaXNEaXJlY3RDaGF0iAEBEjkKFnNlbmRlcl9wcm9maWxl' 'NfZGlyZWN0X2NoYXQYAyABKAhIAVIMaXNEaXJlY3RDaGF0iAEBEjkKFnNlbmRlcl9wcm9maWxl'
'X2NvdW50ZXIYBCABKANIAlIUc2VuZGVyUHJvZmlsZUNvdW50ZXKIAQESRgodc2VuZGVyX3VzZX' 'X2NvdW50ZXIYBCABKANIAlIUc2VuZGVyUHJvZmlsZUNvdW50ZXKIAQESRgodc2VuZGVyX3VzZX'
'JfZGlzY292ZXJ5X3ZlcnNpb24YFSABKAxIA1Iac2VuZGVyVXNlckRpc2NvdmVyeVZlcnNpb26I' 'JfZGlzY292ZXJ5X3ZlcnNpb24YFSABKAxIA1Iac2VuZGVyVXNlckRpc2NvdmVyeVZlcnNpb26I'
'AQESSwoObWVzc2FnZV91cGRhdGUYBSABKAsyHy5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcG' 'AQESPgoZYXNrX2Zvcl9mcmllbmRfcHJvbW90aW9ucxgZIAEoCEgEUhZhc2tGb3JGcmllbmRQcm'
'RhdGVIBFINbWVzc2FnZVVwZGF0ZYgBARIyCgVtZWRpYRgGIAEoCzIXLkVuY3J5cHRlZENvbnRl' '9tb3Rpb25ziAEBEksKDm1lc3NhZ2VfdXBkYXRlGAUgASgLMh8uRW5jcnlwdGVkQ29udGVudC5N'
'bnQuTWVkaWFIBVIFbWVkaWGIAQESRQoMbWVkaWFfdXBkYXRlGAcgASgLMh0uRW5jcnlwdGVkQ2' 'ZXNzYWdlVXBkYXRlSAVSDW1lc3NhZ2VVcGRhdGWIAQESMgoFbWVkaWEYBiABKAsyFy5FbmNyeX'
'9udGVudC5NZWRpYVVwZGF0ZUgGUgttZWRpYVVwZGF0ZYgBARJLCg5jb250YWN0X3VwZGF0ZRgI' 'B0ZWRDb250ZW50Lk1lZGlhSAZSBW1lZGlhiAEBEkUKDG1lZGlhX3VwZGF0ZRgHIAEoCzIdLkVu'
'IAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZUgHUg1jb250YWN0VXBkYXRliA' 'Y3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGVIB1ILbWVkaWFVcGRhdGWIAQESSwoOY29udGFjdF'
'EBEk4KD2NvbnRhY3RfcmVxdWVzdBgJIAEoCzIgLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJl' '91cGRhdGUYCCABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGVICFINY29udGFj'
'cXVlc3RICFIOY29udGFjdFJlcXVlc3SIAQESPwoKZmxhbWVfc3luYxgKIAEoCzIbLkVuY3J5cH' 'dFVwZGF0ZYgBARJOCg9jb250YWN0X3JlcXVlc3QYCSABKAsyIC5FbmNyeXB0ZWRDb250ZW50Lk'
'RlZENvbnRlbnQuRmxhbWVTeW5jSAlSCWZsYW1lU3luY4gBARI8CglwdXNoX2tleXMYCyABKAsy' 'NvbnRhY3RSZXF1ZXN0SAlSDmNvbnRhY3RSZXF1ZXN0iAEBEj8KCmZsYW1lX3N5bmMYCiABKAsy'
'Gi5FbmNyeXB0ZWRDb250ZW50LlB1c2hLZXlzSApSCHB1c2hLZXlziAEBEjsKCHJlYWN0aW9uGA' 'Gy5FbmNyeXB0ZWRDb250ZW50LkZsYW1lU3luY0gKUglmbGFtZVN5bmOIAQESPAoJcHVzaF9rZX'
'wgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgLUghyZWFjdGlvbogBARJFCgx0ZXh0' 'lzGAsgASgLMhouRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5c0gLUghwdXNoS2V5c4gBARI7Cghy'
'X21lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb250ZW50LlRleHRNZXNzYWdlSAxSC3RleHRNZX' 'ZWFjdGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVhY3Rpb25IDFIIcmVhY3Rpb26IAQ'
'NzYWdliAEBEkUKDGdyb3VwX2NyZWF0ZRgOIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuR3JvdXBD' 'ESRQoMdGV4dF9tZXNzYWdlGA0gASgLMh0uRW5jcnlwdGVkQ29udGVudC5UZXh0TWVzc2FnZUgN'
'cmVhdGVIDVILZ3JvdXBDcmVhdGWIAQESPwoKZ3JvdXBfam9pbhgPIAEoCzIbLkVuY3J5cHRlZE' 'Ugt0ZXh0TWVzc2FnZYgBARJFCgxncm91cF9jcmVhdGUYDiABKAsyHS5FbmNyeXB0ZWRDb250ZW'
'NvbnRlbnQuR3JvdXBKb2luSA5SCWdyb3VwSm9pbogBARJFCgxncm91cF91cGRhdGUYECABKAsy' '50Lkdyb3VwQ3JlYXRlSA5SC2dyb3VwQ3JlYXRliAEBEj8KCmdyb3VwX2pvaW4YDyABKAsyGy5F'
'HS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSA9SC2dyb3VwVXBkYXRliAEBEmIKF3Jlc2' 'bmNyeXB0ZWRDb250ZW50Lkdyb3VwSm9pbkgPUglncm91cEpvaW6IAQESRQoMZ3JvdXBfdXBkYX'
'VuZF9ncm91cF9wdWJsaWNfa2V5GBEgASgLMiYuRW5jcnlwdGVkQ29udGVudC5SZXNlbmRHcm91' 'RlGBAgASgLMh0uRW5jcnlwdGVkQ29udGVudC5Hcm91cFVwZGF0ZUgQUgtncm91cFVwZGF0ZYgB'
'cFB1YmxpY0tleUgQUhRyZXNlbmRHcm91cFB1YmxpY0tleYgBARJLCg5lcnJvcl9tZXNzYWdlcx' 'ARJiChdyZXNlbmRfZ3JvdXBfcHVibGljX2tleRgRIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUm'
'gSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlc0gRUg1lcnJvck1lc3NhZ2Vz' 'VzZW5kR3JvdXBQdWJsaWNLZXlIEVIUcmVzZW5kR3JvdXBQdWJsaWNLZXmIAQESSwoOZXJyb3Jf'
'iAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgASgLMicuRW5jcnlwdGVkQ29udGVudC' 'bWVzc2FnZXMYEiABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXNIElINZXJyb3'
'5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIElIVYWRkaXRpb25hbERhdGFNZXNzYWdliAEBElEKEHR5' 'JNZXNzYWdlc4gBARJkChdhZGRpdGlvbmFsX2RhdGFfbWVzc2FnZRgTIAEoCzInLkVuY3J5cHRl'
'cGluZ19pbmRpY2F0b3IYFCABKAsyIS5FbmNyeXB0ZWRDb250ZW50LlR5cGluZ0luZGljYXRvck' 'ZENvbnRlbnQuQWRkaXRpb25hbERhdGFNZXNzYWdlSBNSFWFkZGl0aW9uYWxEYXRhTWVzc2FnZY'
'gTUg90eXBpbmdJbmRpY2F0b3KIAQESYQoWdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdBgWIAEoCzIm' 'gBARJRChB0eXBpbmdfaW5kaWNhdG9yGBQgASgLMiEuRW5jcnlwdGVkQ29udGVudC5UeXBpbmdJ'
'LkVuY3J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVJlcXVlc3RIFFIUdXNlckRpc2NvdmVyeV' 'bmRpY2F0b3JIFFIPdHlwaW5nSW5kaWNhdG9yiAEBEmEKFnVzZXJfZGlzY292ZXJ5X3JlcXVlc3'
'JlcXVlc3SIAQESXgoVdXNlcl9kaXNjb3ZlcnlfdXBkYXRlGBcgASgLMiUuRW5jcnlwdGVkQ29u' 'QYFiABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlVzZXJEaXNjb3ZlcnlSZXF1ZXN0SBVSFHVzZXJE'
'dGVudC5Vc2VyRGlzY292ZXJ5VXBkYXRlSBVSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQESYQoWa2' 'aXNjb3ZlcnlSZXF1ZXN0iAEBEl4KFXVzZXJfZGlzY292ZXJ5X3VwZGF0ZRgXIAEoCzIlLkVuY3'
'V5X3ZlcmlmaWNhdGlvbl9wcm9vZhgYIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuS2V5VmVyaWZp' 'J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVVwZGF0ZUgWUhN1c2VyRGlzY292ZXJ5VXBkYXRl'
'Y2F0aW9uUHJvb2ZIFlIUa2V5VmVyaWZpY2F0aW9uUHJvb2aIAQEa8AEKDUVycm9yTWVzc2FnZX' 'iAEBEmEKFmtleV92ZXJpZmljYXRpb25fcHJvb2YYGCABKAsyJi5FbmNyeXB0ZWRDb250ZW50Lk'
'MSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeXBlUgR0' 'tleVZlcmlmaWNhdGlvblByb29mSBdSFGtleVZlcmlmaWNhdGlvblByb29miAEBEmAKFXBhc3N3'
'eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCJ3CgRUeX' 'b3JkbGVzc19yZWNvdmVyeRgaIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUm'
'BlEjwKOEVSUk9SX1BST0NFU1NJTkdfTUVTU0FHRV9DUkVBVEVEX0FDQ09VTlRfUkVRVUVTVF9J' 'Vjb3ZlcnlIGFIUcGFzc3dvcmRsZXNzUmVjb3ZlcnmIAQESfAofcGFzc3dvcmRsZXNzX3JlY292'
'TlNURUFEEAASGAoUVU5LTk9XTl9NRVNTQUdFX1RZUEUQAhIXChNTRVNTSU9OX09VVF9PRl9TWU' 'ZXJ5X2hlYXJ0YmVhdBgbIAEoCzIvLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUmVjb3'
'5DEAMaVAoLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91' 'ZlcnlIZWFydGJlYXRIGVIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlIZWFydGJlYXSIAQEalgIKDUVy'
'cF9wdWJsaWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRo1CglHcm91cEpvaW4SKAoQZ3JvdX' 'cm9yTWVzc2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYW'
'BfcHVibGljX2tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNL' 'dlcy5UeXBlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2Vp'
'ZXkayAIKC0dyb3VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdG' 'cHRJZCKcAQoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU'
'lvblR5cGUSMwoTYWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJ' '5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVTU0FHRV9UWVBFEAISFwoTU0VTU0lP'
'ZIgBARIpCg5uZXdfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2' 'Tl9PVVRfT0ZfU1lOQxADEiMKH0dST1VQX05PVF9GT1VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQ'
'RlbGV0ZV9tZXNzYWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVz' 'oLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91cF9wdWJs'
'c2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbm' 'aWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRIiCgpncm91cF9uYW1lGAUgASgJSABSCWdyb3'
'V3X2dyb3VwX25hbWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRz' 'VwTmFtZYgBAUINCgtfZ3JvdXBfbmFtZRo1CglHcm91cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tl'
'Gq8BCgtUZXh0TWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2' 'eRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3'
'FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXAS' 'VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMwoT'
'LQoQcXVvdGVfbWVzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUITChFfcXVvdG' 'YWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBARIpCg5uZX'
'VfbWVzc2FnZV9pZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNlbmRlcl9tZXNzYWdl' 'dfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2RlbGV0ZV9tZXNz'
'X2lkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgASgDUgl0aW1lc3RhbX' 'YWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck'
'ASEgoEdHlwZRgDIAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgEIAEoDEgA' '1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbmV3X2dyb3VwX25h'
'UhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGm' 'bWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGq8BCgtUZXh0TW'
'QKCFJlYWN0aW9uEioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQS' 'Vzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRl'
'FAoFZW1vamkYAiABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGr4CCg1NZXNzYW' 'eHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbW'
'dlVXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUu' 'Vzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBAUITChFfcXVvdGVfbWVzc2FnZV9p'
'VHlwZVIEdHlwZRIvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSW' 'ZBrOAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg'
'SIAQESPQobbXVsdGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhhtdWx0aXBsZVRhcmdl' '9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1wGAIgASgDUgl0aW1lc3RhbXASEgoEdHlwZRgD'
'dE1lc3NhZ2VJZHMSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1' 'IAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgEIAEoDEgAUhVhZGRpdGlvbm'
'IJdGltZXN0YW1wIi0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVE' 'FsTWVzc2FnZURhdGGIAQFCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGmQKCFJlYWN0aW9u'
'EAJCFAoSX3NlbmRlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYRIqChFzZW5kZXJfbW' 'EioKEXRhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFAoFZW1vamkYAi'
'Vzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0' 'ABKAlSBWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGr4CCg1NZXNzYWdlVXBkYXRlEjgK'
'ZWRDb250ZW50Lk1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY2' 'BHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVHlwZVIEdHlwZR'
'9uZHMYAyABKANIAFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNwoXcmVxdWlyZXNf' 'IvChFzZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESPQobbXVs'
'YXV0aGVudGljYXRpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZXN0YW' 'dGlwbGVfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZH'
'1wGAUgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEoCUgBUg5xdW90ZU1l' 'MSFwoEdGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1w'
'c3NhZ2VJZIgBARIqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEi' 'Ii0KBFR5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCFAoSX3Nlbm'
'oKDmVuY3J5cHRpb25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQESKgoOZW5jcnlwdGlv' 'Rlcl9tZXNzYWdlX2lkQgcKBV90ZXh0GoUGCgVNZWRpYRIqChFzZW5kZXJfbWVzc2FnZV9pZBgB'
'bl9tYWMYCSABKAxIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW9uX25vbmNlGAogAS' 'IAEoCVIPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk'
'gMSAVSD2VuY3J5cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgLIAEo' '1lZGlhLlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHMYAyABKANI'
'DEgGUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRVVQTE9BRBAAEgkKBU' 'AFIaZGlzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNwoXcmVxdWlyZXNfYXV0aGVudGljYX'
'lNQUdFEAESCQoFVklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9kaXNwbGF5X2xpbWl0' 'Rpb24YBCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZXN0YW1wGAUgASgDUgl0'
'X2luX21pbGxpc2Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG93bmxvYWRfdG9rZW' 'aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBAR'
'5CEQoPX2VuY3J5cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFfZW5jcnlwdGlvbl9u' 'IqCg5kb3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEioKDmVuY3J5cHRp'
'b25jZUIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVXBkYXRlEjYKBHR5cG' 'b25fa2V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQESKgoOZW5jcnlwdGlvbl9tYWMYCSABKA'
'UYASABKA4yIi5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKgoRdGFy' 'xIBFINZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW9uX25vbmNlGAogASgMSAVSD2VuY3J5'
'Z2V0X21lc3NhZ2VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTk' 'cHRpb25Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgLIAEoDEgGUhVhZGRpdG'
'VEEAASCgoGU1RPUkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0' 'lvbmFsTWVzc2FnZURhdGGIAQEiPgoEVHlwZRIMCghSRVVQTE9BRBAAEgkKBUlNQUdFEAESCQoF'
'EjkKBHR5cGUYASABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBH' 'VklERU8QAhIHCgNHSUYQAxIJCgVBVURJTxAEQiAKHl9kaXNwbGF5X2xpbWl0X2luX21pbGxpc2'
'R5cGUiKwoEVHlwZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIapAIKDUNv' 'Vjb25kc0ITChFfcXVvdGVfbWVzc2FnZV9pZEIRCg9fZG93bmxvYWRfdG9rZW5CEQoPX2VuY3J5'
'bnRhY3RVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZG' 'cHRpb25fa2V5QhEKD19lbmNyeXB0aW9uX21hY0ITChFfZW5jcnlwdGlvbl9ub25jZUIaChhfYW'
'F0ZS5UeXBlUgR0eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJT' 'RkaXRpb25hbF9tZXNzYWdlX2RhdGEaqQEKC01lZGlhVXBkYXRlEjYKBHR5cGUYASABKA4yIi5F'
'dmdDb21wcmVzc2VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiYKDGRpc3' 'bmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlLlR5cGVSBHR5cGUSKgoRdGFyZ2V0X21lc3NhZ2'
'BsYXlfbmFtZRgEIAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIK' 'VfaWQYAiABKAlSD3RhcmdldE1lc3NhZ2VJZCI2CgRUeXBlEgwKCFJFT1BFTkVEEAASCgoGU1RP'
'CgZVUERBVEUQAUIYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2VybmFtZUIPCg1fZG' 'UkVEEAESFAoQREVDUllQVElPTl9FUlJPUhACGngKDkNvbnRhY3RSZXF1ZXN0EjkKBHR5cGUYAS'
'lzcGxheV9uYW1lGtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVu' 'ABKA4yJS5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RSZXF1ZXN0LlR5cGVSBHR5cGUiKwoEVHlw'
'dC5QdXNoS2V5cy5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZX' 'ZRILCgdSRVFVRVNUEAASCgoGUkVKRUNUEAESCgoGQUNDRVBUEAIapAIKDUNvbnRhY3RVcGRhdG'
'kYAyABKAxIAVIDa2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8K' 'USOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZS5UeXBlUgR0'
'BFR5cGUSCwoHUkVRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBgoEX2tleUINCgtfY3' 'eXBlEjcKFWF2YXRhcl9zdmdfY29tcHJlc3NlZBgCIAEoDEgAUhNhdmF0YXJTdmdDb21wcmVzc2'
'JlYXRlZF9hdBqvAQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291' 'VkiAEBEh8KCHVzZXJuYW1lGAMgASgJSAFSCHVzZXJuYW1liAEBEiYKDGRpc3BsYXlfbmFtZRgE'
'bnRlchI5ChlsYXN0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudG' 'IAEoCUgCUgtkaXNwbGF5TmFtZYgBASIfCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZVUERBVEUQAU'
'VyQ2hhbmdlEh8KC2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0' 'IYChZfYXZhdGFyX3N2Z19jb21wcmVzc2VkQgsKCV91c2VybmFtZUIPCg1fZGlzcGxheV9uYW1l'
'ZRgEIAEoCFILZm9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIA' 'GtkBCghQdXNoS2V5cxIzCgR0eXBlGAEgASgOMh8uRW5jcnlwdGVkQ29udGVudC5QdXNoS2V5cy'
'EoCFIIaXNUeXBpbmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNj' '5UeXBlUgR0eXBlEhoKBmtleV9pZBgCIAEoA0gAUgVrZXlJZIgBARIVCgNrZXkYAyABKAxIAVID'
'b3ZlcnlSZXF1ZXN0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQ' 'a2V5iAEBEiIKCmNyZWF0ZWRfYXQYBCABKANIAlIJY3JlYXRlZEF0iAEBIh8KBFR5cGUSCwoHUk'
'oTVXNlckRpc2NvdmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5' 'VRVUVTVBAAEgoKBlVQREFURRABQgkKB19rZXlfaWRCBgoEX2tleUINCgtfY3JlYXRlZF9hdBqv'
'VmVyaWZpY2F0aW9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYW' 'AQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDGZsYW1lQ291bnRlchI5ChlsYX'
'NCCwoJX2dyb3VwX2lkQhEKD19pc19kaXJlY3RfY2hhdEIZChdfc2VuZGVyX3Byb2ZpbGVfY291' 'N0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0RmxhbWVDb3VudGVyQ2hhbmdlEh8K'
'bnRlckIgCh5fc2VuZGVyX3VzZXJfZGlzY292ZXJ5X3ZlcnNpb25CEQoPX21lc3NhZ2VfdXBkYX' 'C2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcmNlX3VwZGF0ZRgEIAEoCFILZm'
'RlQggKBl9tZWRpYUIPCg1fbWVkaWFfdXBkYXRlQhEKD19jb250YWN0X3VwZGF0ZUISChBfY29u' '9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5cGluZxgBIAEoCFIIaXNUeXBp'
'dGFjdF9yZXF1ZXN0Qg0KC19mbGFtZV9zeW5jQgwKCl9wdXNoX2tleXNCCwoJX3JlYWN0aW9uQg' 'bmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFFVzZXJEaXNjb3ZlcnlSZXF1ZX'
'8KDV90ZXh0X21lc3NhZ2VCDwoNX2dyb3VwX2NyZWF0ZUINCgtfZ3JvdXBfam9pbkIPCg1fZ3Jv' 'N0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZlcnNpb24aMQoTVXNlckRpc2Nv'
'dXBfdXBkYXRlQhoKGF9yZXNlbmRfZ3JvdXBfcHVibGljX2tleUIRCg9fZXJyb3JfbWVzc2FnZX' 'dmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZXMaPQoUS2V5VmVyaWZpY2F0aW'
'NCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlQhMKEV90eXBpbmdfaW5kaWNhdG9yQhkKF191' '9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1bGF0ZWRNYWMamwEKFFBhc3N3'
'c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0QhgKFl91c2VyX2Rpc2NvdmVyeV91cGRhdGVCGQoXX2tleV' 'b3JkTGVzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcmUYASABKAxIAFITcmVjb3Zlcn'
'92ZXJpZmljYXRpb25fcHJvb2Y='); 'lTZWNyZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0ZRIcCgl0aHJlc2hvbGQYAyAB'
'KANSCXRocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZRozCh1QYXNzd29yZExlc3NSZW'
'NvdmVyeUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsKCV9ncm91cF9pZEIRCg9faXNf'
'ZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZXJCIAoeX3NlbmRlcl91c2VyX2'
'Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9wcm9tb3Rpb25zQhEKD19tZXNz'
'YWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9fY29udGFjdF91cGRhdG'
'VCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0IMCgpfcHVzaF9rZXlzQgsKCV9y'
'ZWFjdGlvbkIPCg1fdGV4dF9tZXNzYWdlQg8KDV9ncm91cF9jcmVhdGVCDQoLX2dyb3VwX2pvaW'
'5CDwoNX2dyb3VwX3VwZGF0ZUIaChhfcmVzZW5kX2dyb3VwX3B1YmxpY19rZXlCEQoPX2Vycm9y'
'X21lc3NhZ2VzQhoKGF9hZGRpdGlvbmFsX2RhdGFfbWVzc2FnZUITChFfdHlwaW5nX2luZGljYX'
'RvckIZChdfdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdEIYChZfdXNlcl9kaXNjb3ZlcnlfdXBkYXRl'
'QhkKF19rZXlfdmVyaWZpY2F0aW9uX3Byb29mQhgKFl9wYXNzd29yZGxlc3NfcmVjb3ZlcnlCIg'
'ogX3Bhc3N3b3JkbGVzc19yZWNvdmVyeV9oZWFydGJlYXQ=');

View file

@ -0,0 +1,544 @@
// This is a generated file - do not edit.
//
// Generated from passwordless_recovery.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb;
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
/// Send from the person who tries to recover their account.
/// This can be done via a link, which will then be opend in the app of the contact.
/// The contact than has to manualy select from which user he got the request.
/// -> Using this phishing is harder, as the user has to manualy select the user to recovery
/// -> The user who wants to recover his account does not need to remember her old username
class RecoveryRequest extends $pb.GeneratedMessage {
factory RecoveryRequest({
$core.String? notificationId,
$core.List<$core.int>? publicKey,
}) {
final result = create();
if (notificationId != null) result.notificationId = notificationId;
if (publicKey != null) result.publicKey = publicKey;
return result;
}
RecoveryRequest._();
factory RecoveryRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory RecoveryRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'RecoveryRequest',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'notificationId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
RecoveryRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
RecoveryRequest copyWith(void Function(RecoveryRequest) updates) =>
super.copyWith((message) => updates(message as RecoveryRequest))
as RecoveryRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static RecoveryRequest create() => RecoveryRequest._();
@$core.override
RecoveryRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static RecoveryRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<RecoveryRequest>(create);
static RecoveryRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.String get notificationId => $_getSZ(0);
@$pb.TagNumber(1)
set notificationId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasNotificationId() => $_has(0);
@$pb.TagNumber(1)
void clearNotificationId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get publicKey => $_getN(1);
@$pb.TagNumber(2)
set publicKey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasPublicKey() => $_has(1);
@$pb.TagNumber(2)
void clearPublicKey() => $_clearField(2);
}
/// Used as envelope for TrustedFriendShare and RecoveryData
class EncryptedEnvelope extends $pb.GeneratedMessage {
factory EncryptedEnvelope({
$core.List<$core.int>? encryptedData,
$core.List<$core.int>? iv,
$core.List<$core.int>? mac,
}) {
final result = create();
if (encryptedData != null) result.encryptedData = encryptedData;
if (iv != null) result.iv = iv;
if (mac != null) result.mac = mac;
return result;
}
EncryptedEnvelope._();
factory EncryptedEnvelope.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EncryptedEnvelope.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EncryptedEnvelope',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'encryptedData', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'iv', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
3, _omitFieldNames ? '' : 'mac', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedEnvelope clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedEnvelope copyWith(void Function(EncryptedEnvelope) updates) =>
super.copyWith((message) => updates(message as EncryptedEnvelope))
as EncryptedEnvelope;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EncryptedEnvelope create() => EncryptedEnvelope._();
@$core.override
EncryptedEnvelope createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static EncryptedEnvelope getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EncryptedEnvelope>(create);
static EncryptedEnvelope? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get encryptedData => $_getN(0);
@$pb.TagNumber(1)
set encryptedData($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasEncryptedData() => $_has(0);
@$pb.TagNumber(1)
void clearEncryptedData() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get iv => $_getN(1);
@$pb.TagNumber(2)
set iv($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasIv() => $_has(1);
@$pb.TagNumber(2)
void clearIv() => $_clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get mac => $_getN(2);
@$pb.TagNumber(3)
set mac($core.List<$core.int> value) => $_setBytes(2, value);
@$pb.TagNumber(3)
$core.bool hasMac() => $_has(2);
@$pb.TagNumber(3)
void clearMac() => $_clearField(3);
}
class TrustedFriendShare_User extends $pb.GeneratedMessage {
factory TrustedFriendShare_User({
$fixnum.Int64? userId,
$core.String? displayName,
$core.List<$core.int>? avatar,
}) {
final result = create();
if (userId != null) result.userId = userId;
if (displayName != null) result.displayName = displayName;
if (avatar != null) result.avatar = avatar;
return result;
}
TrustedFriendShare_User._();
factory TrustedFriendShare_User.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory TrustedFriendShare_User.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'TrustedFriendShare.User',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'userId')
..aOS(2, _omitFieldNames ? '' : 'displayName')
..a<$core.List<$core.int>>(
3, _omitFieldNames ? '' : 'avatar', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
TrustedFriendShare_User clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
TrustedFriendShare_User copyWith(
void Function(TrustedFriendShare_User) updates) =>
super.copyWith((message) => updates(message as TrustedFriendShare_User))
as TrustedFriendShare_User;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static TrustedFriendShare_User create() => TrustedFriendShare_User._();
@$core.override
TrustedFriendShare_User createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static TrustedFriendShare_User getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<TrustedFriendShare_User>(create);
static TrustedFriendShare_User? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get userId => $_getI64(0);
@$pb.TagNumber(1)
set userId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasUserId() => $_has(0);
@$pb.TagNumber(1)
void clearUserId() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get displayName => $_getSZ(1);
@$pb.TagNumber(2)
set displayName($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasDisplayName() => $_has(1);
@$pb.TagNumber(2)
void clearDisplayName() => $_clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get avatar => $_getN(2);
@$pb.TagNumber(3)
set avatar($core.List<$core.int> value) => $_setBytes(2, value);
@$pb.TagNumber(3)
$core.bool hasAvatar() => $_has(2);
@$pb.TagNumber(3)
void clearAvatar() => $_clearField(3);
}
/// Send from the trusted friend to
/// This is encrypted with the received public key.
class TrustedFriendShare extends $pb.GeneratedMessage {
factory TrustedFriendShare({
TrustedFriendShare_User? trustedFriend,
TrustedFriendShare_User? shareUser,
$core.int? threshold,
$core.List<$core.int>? sharedSecretData,
}) {
final result = create();
if (trustedFriend != null) result.trustedFriend = trustedFriend;
if (shareUser != null) result.shareUser = shareUser;
if (threshold != null) result.threshold = threshold;
if (sharedSecretData != null) result.sharedSecretData = sharedSecretData;
return result;
}
TrustedFriendShare._();
factory TrustedFriendShare.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory TrustedFriendShare.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'TrustedFriendShare',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..aOM<TrustedFriendShare_User>(1, _omitFieldNames ? '' : 'trustedFriend',
subBuilder: TrustedFriendShare_User.create)
..aOM<TrustedFriendShare_User>(2, _omitFieldNames ? '' : 'shareUser',
subBuilder: TrustedFriendShare_User.create)
..aI(3, _omitFieldNames ? '' : 'threshold')
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'sharedSecretData', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
TrustedFriendShare clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
TrustedFriendShare copyWith(void Function(TrustedFriendShare) updates) =>
super.copyWith((message) => updates(message as TrustedFriendShare))
as TrustedFriendShare;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static TrustedFriendShare create() => TrustedFriendShare._();
@$core.override
TrustedFriendShare createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static TrustedFriendShare getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<TrustedFriendShare>(create);
static TrustedFriendShare? _defaultInstance;
/// This allows to display the user which user has send him his recovery data.
@$pb.TagNumber(1)
TrustedFriendShare_User get trustedFriend => $_getN(0);
@$pb.TagNumber(1)
set trustedFriend(TrustedFriendShare_User value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasTrustedFriend() => $_has(0);
@$pb.TagNumber(1)
void clearTrustedFriend() => $_clearField(1);
@$pb.TagNumber(1)
TrustedFriendShare_User ensureTrustedFriend() => $_ensure(0);
/// This allows to display the userdata, showing that he is recovering the correct person.
@$pb.TagNumber(2)
TrustedFriendShare_User get shareUser => $_getN(1);
@$pb.TagNumber(2)
set shareUser(TrustedFriendShare_User value) => $_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasShareUser() => $_has(1);
@$pb.TagNumber(2)
void clearShareUser() => $_clearField(2);
@$pb.TagNumber(2)
TrustedFriendShare_User ensureShareUser() => $_ensure(1);
/// The minimum threshold required to decrypte the shares.
@$pb.TagNumber(3)
$core.int get threshold => $_getIZ(2);
@$pb.TagNumber(3)
set threshold($core.int value) => $_setSignedInt32(2, value);
@$pb.TagNumber(3)
$core.bool hasThreshold() => $_has(2);
@$pb.TagNumber(3)
void clearThreshold() => $_clearField(3);
/// The actual share which will become: SharedSecretData
@$pb.TagNumber(4)
$core.List<$core.int> get sharedSecretData => $_getN(3);
@$pb.TagNumber(4)
set sharedSecretData($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasSharedSecretData() => $_has(3);
@$pb.TagNumber(4)
void clearSharedSecretData() => $_clearField(4);
}
/// The user identity keys. Can be used to restore the data backup.
class RecoveryData extends $pb.GeneratedMessage {
factory RecoveryData({
$fixnum.Int64? userId,
$core.List<$core.int>? keyManager,
}) {
final result = create();
if (userId != null) result.userId = userId;
if (keyManager != null) result.keyManager = keyManager;
return result;
}
RecoveryData._();
factory RecoveryData.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory RecoveryData.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'RecoveryData',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'userId')
..a<$core.List<$core.int>>(
3, _omitFieldNames ? '' : 'keyManager', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
RecoveryData clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
RecoveryData copyWith(void Function(RecoveryData) updates) =>
super.copyWith((message) => updates(message as RecoveryData))
as RecoveryData;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static RecoveryData create() => RecoveryData._();
@$core.override
RecoveryData createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static RecoveryData getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<RecoveryData>(create);
static RecoveryData? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get userId => $_getI64(0);
@$pb.TagNumber(1)
set userId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasUserId() => $_has(0);
@$pb.TagNumber(1)
void clearUserId() => $_clearField(1);
@$pb.TagNumber(3)
$core.List<$core.int> get keyManager => $_getN(1);
@$pb.TagNumber(3)
set keyManager($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(3)
$core.bool hasKeyManager() => $_has(1);
@$pb.TagNumber(3)
void clearKeyManager() => $_clearField(3);
}
/// After received all shares this is decrypted by the user restoring its own
class SharedSecretData extends $pb.GeneratedMessage {
factory SharedSecretData({
$core.List<$core.int>? recoveryData,
$core.List<$core.int>? encryptedServerKeyNonce,
$core.List<$core.int>? pinSeed,
$core.List<$core.int>? pinUnlockToken,
$core.String? emailHint,
}) {
final result = create();
if (recoveryData != null) result.recoveryData = recoveryData;
if (encryptedServerKeyNonce != null)
result.encryptedServerKeyNonce = encryptedServerKeyNonce;
if (pinSeed != null) result.pinSeed = pinSeed;
if (pinUnlockToken != null) result.pinUnlockToken = pinUnlockToken;
if (emailHint != null) result.emailHint = emailHint;
return result;
}
SharedSecretData._();
factory SharedSecretData.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory SharedSecretData.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'SharedSecretData',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'recoveryData', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
3, _omitFieldNames ? '' : 'encryptedServerKeyNonce', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'pinSeed', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
5, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY)
..aOS(6, _omitFieldNames ? '' : 'emailHint')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SharedSecretData clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SharedSecretData copyWith(void Function(SharedSecretData) updates) =>
super.copyWith((message) => updates(message as SharedSecretData))
as SharedSecretData;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static SharedSecretData create() => SharedSecretData._();
@$core.override
SharedSecretData createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static SharedSecretData getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<SharedSecretData>(create);
static SharedSecretData? _defaultInstance;
/// The recovery data is encrypted in case a second factor was chosen.
@$pb.TagNumber(1)
$core.List<$core.int> get recoveryData => $_getN(0);
@$pb.TagNumber(1)
set recoveryData($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasRecoveryData() => $_has(0);
@$pb.TagNumber(1)
void clearRecoveryData() => $_clearField(1);
@$pb.TagNumber(3)
$core.List<$core.int> get encryptedServerKeyNonce => $_getN(1);
@$pb.TagNumber(3)
set encryptedServerKeyNonce($core.List<$core.int> value) =>
$_setBytes(1, value);
@$pb.TagNumber(3)
$core.bool hasEncryptedServerKeyNonce() => $_has(1);
@$pb.TagNumber(3)
void clearEncryptedServerKeyNonce() => $_clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get pinSeed => $_getN(2);
@$pb.TagNumber(4)
set pinSeed($core.List<$core.int> value) => $_setBytes(2, value);
@$pb.TagNumber(4)
$core.bool hasPinSeed() => $_has(2);
@$pb.TagNumber(4)
void clearPinSeed() => $_clearField(4);
@$pb.TagNumber(5)
$core.List<$core.int> get pinUnlockToken => $_getN(3);
@$pb.TagNumber(5)
set pinUnlockToken($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(5)
$core.bool hasPinUnlockToken() => $_has(3);
@$pb.TagNumber(5)
void clearPinUnlockToken() => $_clearField(5);
@$pb.TagNumber(6)
$core.String get emailHint => $_getSZ(4);
@$pb.TagNumber(6)
set emailHint($core.String value) => $_setString(4, value);
@$pb.TagNumber(6)
$core.bool hasEmailHint() => $_has(4);
@$pb.TagNumber(6)
void clearEmailHint() => $_clearField(6);
}
const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');

View file

@ -0,0 +1,11 @@
// This is a generated file - do not edit.
//
// Generated from passwordless_recovery.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports

View file

@ -0,0 +1,170 @@
// This is a generated file - do not edit.
//
// Generated from passwordless_recovery.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
// ignore_for_file: unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use recoveryRequestDescriptor instead')
const RecoveryRequest$json = {
'1': 'RecoveryRequest',
'2': [
{'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'},
{'1': 'public_key', '3': 2, '4': 1, '5': 12, '10': 'publicKey'},
],
};
/// Descriptor for `RecoveryRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List recoveryRequestDescriptor = $convert.base64Decode(
'Cg9SZWNvdmVyeVJlcXVlc3QSJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb2'
'5JZBIdCgpwdWJsaWNfa2V5GAIgASgMUglwdWJsaWNLZXk=');
@$core.Deprecated('Use encryptedEnvelopeDescriptor instead')
const EncryptedEnvelope$json = {
'1': 'EncryptedEnvelope',
'2': [
{'1': 'encrypted_data', '3': 1, '4': 1, '5': 12, '10': 'encryptedData'},
{'1': 'iv', '3': 2, '4': 1, '5': 12, '10': 'iv'},
{'1': 'mac', '3': 3, '4': 1, '5': 12, '10': 'mac'},
],
};
/// Descriptor for `EncryptedEnvelope`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List encryptedEnvelopeDescriptor = $convert.base64Decode(
'ChFFbmNyeXB0ZWRFbnZlbG9wZRIlCg5lbmNyeXB0ZWRfZGF0YRgBIAEoDFINZW5jcnlwdGVkRG'
'F0YRIOCgJpdhgCIAEoDFICaXYSEAoDbWFjGAMgASgMUgNtYWM=');
@$core.Deprecated('Use trustedFriendShareDescriptor instead')
const TrustedFriendShare$json = {
'1': 'TrustedFriendShare',
'2': [
{
'1': 'trusted_friend',
'3': 1,
'4': 1,
'5': 11,
'6': '.passwordless_recovery.TrustedFriendShare.User',
'10': 'trustedFriend'
},
{
'1': 'share_user',
'3': 2,
'4': 1,
'5': 11,
'6': '.passwordless_recovery.TrustedFriendShare.User',
'10': 'shareUser'
},
{'1': 'threshold', '3': 3, '4': 1, '5': 5, '10': 'threshold'},
{
'1': 'shared_secret_data',
'3': 4,
'4': 1,
'5': 12,
'10': 'sharedSecretData'
},
],
'3': [TrustedFriendShare_User$json],
};
@$core.Deprecated('Use trustedFriendShareDescriptor instead')
const TrustedFriendShare_User$json = {
'1': 'User',
'2': [
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
{'1': 'display_name', '3': 2, '4': 1, '5': 9, '10': 'displayName'},
{'1': 'avatar', '3': 3, '4': 1, '5': 12, '10': 'avatar'},
],
};
/// Descriptor for `TrustedFriendShare`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List trustedFriendShareDescriptor = $convert.base64Decode(
'ChJUcnVzdGVkRnJpZW5kU2hhcmUSVQoOdHJ1c3RlZF9mcmllbmQYASABKAsyLi5wYXNzd29yZG'
'xlc3NfcmVjb3ZlcnkuVHJ1c3RlZEZyaWVuZFNoYXJlLlVzZXJSDXRydXN0ZWRGcmllbmQSTQoK'
'c2hhcmVfdXNlchgCIAEoCzIuLnBhc3N3b3JkbGVzc19yZWNvdmVyeS5UcnVzdGVkRnJpZW5kU2'
'hhcmUuVXNlclIJc2hhcmVVc2VyEhwKCXRocmVzaG9sZBgDIAEoBVIJdGhyZXNob2xkEiwKEnNo'
'YXJlZF9zZWNyZXRfZGF0YRgEIAEoDFIQc2hhcmVkU2VjcmV0RGF0YRpaCgRVc2VyEhcKB3VzZX'
'JfaWQYASABKANSBnVzZXJJZBIhCgxkaXNwbGF5X25hbWUYAiABKAlSC2Rpc3BsYXlOYW1lEhYK'
'BmF2YXRhchgDIAEoDFIGYXZhdGFy');
@$core.Deprecated('Use recoveryDataDescriptor instead')
const RecoveryData$json = {
'1': 'RecoveryData',
'2': [
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
{'1': 'key_manager', '3': 3, '4': 1, '5': 12, '10': 'keyManager'},
],
};
/// Descriptor for `RecoveryData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List recoveryDataDescriptor = $convert.base64Decode(
'CgxSZWNvdmVyeURhdGESFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEh8KC2tleV9tYW5hZ2VyGA'
'MgASgMUgprZXlNYW5hZ2Vy');
@$core.Deprecated('Use sharedSecretDataDescriptor instead')
const SharedSecretData$json = {
'1': 'SharedSecretData',
'2': [
{'1': 'recovery_data', '3': 1, '4': 1, '5': 12, '10': 'recoveryData'},
{
'1': 'encrypted_server_key_nonce',
'3': 3,
'4': 1,
'5': 12,
'9': 0,
'10': 'encryptedServerKeyNonce',
'17': true
},
{
'1': 'pin_seed',
'3': 4,
'4': 1,
'5': 12,
'9': 1,
'10': 'pinSeed',
'17': true
},
{
'1': 'pin_unlock_token',
'3': 5,
'4': 1,
'5': 12,
'9': 2,
'10': 'pinUnlockToken',
'17': true
},
{
'1': 'email_hint',
'3': 6,
'4': 1,
'5': 9,
'9': 3,
'10': 'emailHint',
'17': true
},
],
'8': [
{'1': '_encrypted_server_key_nonce'},
{'1': '_pin_seed'},
{'1': '_pin_unlock_token'},
{'1': '_email_hint'},
],
};
/// Descriptor for `SharedSecretData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sharedSecretDataDescriptor = $convert.base64Decode(
'ChBTaGFyZWRTZWNyZXREYXRhEiMKDXJlY292ZXJ5X2RhdGEYASABKAxSDHJlY292ZXJ5RGF0YR'
'JAChplbmNyeXB0ZWRfc2VydmVyX2tleV9ub25jZRgDIAEoDEgAUhdlbmNyeXB0ZWRTZXJ2ZXJL'
'ZXlOb25jZYgBARIeCghwaW5fc2VlZBgEIAEoDEgBUgdwaW5TZWVkiAEBEi0KEHBpbl91bmxvY2'
'tfdG9rZW4YBSABKAxIAlIOcGluVW5sb2NrVG9rZW6IAQESIgoKZW1haWxfaGludBgGIAEoCUgD'
'UgllbWFpbEhpbnSIAQFCHQobX2VuY3J5cHRlZF9zZXJ2ZXJfa2V5X25vbmNlQgsKCV9waW5fc2'
'VlZEITChFfcGluX3VubG9ja190b2tlbkINCgtfZW1haWxfaGludA==');

View file

@ -0,0 +1,670 @@
// This is a generated file - do not edit.
//
// Generated from types.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb;
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
/// Send from the person who tries to recover their account.
/// This can be done via a link, which will then be opend in the app of the contact.
/// The contact than has to manualy select from which user he got the request.
/// -> Using this phishing is harder, as the user has to manualy select the user to recovery
/// -> The user who wants to recover his account does not need to remember her old username
class RecoveryRequest extends $pb.GeneratedMessage {
factory RecoveryRequest({
$fixnum.Int64? tempId,
$core.List<$core.int>? publicKey,
}) {
final result = create();
if (tempId != null) result.tempId = tempId;
if (publicKey != null) result.publicKey = publicKey;
return result;
}
RecoveryRequest._();
factory RecoveryRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory RecoveryRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'RecoveryRequest',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'tempId')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
RecoveryRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
RecoveryRequest copyWith(void Function(RecoveryRequest) updates) =>
super.copyWith((message) => updates(message as RecoveryRequest))
as RecoveryRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static RecoveryRequest create() => RecoveryRequest._();
@$core.override
RecoveryRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static RecoveryRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<RecoveryRequest>(create);
static RecoveryRequest? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get tempId => $_getI64(0);
@$pb.TagNumber(1)
set tempId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasTempId() => $_has(0);
@$pb.TagNumber(1)
void clearTempId() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get publicKey => $_getN(1);
@$pb.TagNumber(2)
set publicKey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasPublicKey() => $_has(1);
@$pb.TagNumber(2)
void clearPublicKey() => $_clearField(2);
}
/// Used as envelope for TrustedFriendShare and RecoveryData
class EncryptedEnvelope extends $pb.GeneratedMessage {
factory EncryptedEnvelope({
$core.List<$core.int>? encryptedData,
$core.List<$core.int>? iv,
$core.List<$core.int>? mac,
}) {
final result = create();
if (encryptedData != null) result.encryptedData = encryptedData;
if (iv != null) result.iv = iv;
if (mac != null) result.mac = mac;
return result;
}
EncryptedEnvelope._();
factory EncryptedEnvelope.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EncryptedEnvelope.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EncryptedEnvelope',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'encryptedData', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'iv', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
3, _omitFieldNames ? '' : 'mac', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedEnvelope clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EncryptedEnvelope copyWith(void Function(EncryptedEnvelope) updates) =>
super.copyWith((message) => updates(message as EncryptedEnvelope))
as EncryptedEnvelope;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EncryptedEnvelope create() => EncryptedEnvelope._();
@$core.override
EncryptedEnvelope createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static EncryptedEnvelope getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EncryptedEnvelope>(create);
static EncryptedEnvelope? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get encryptedData => $_getN(0);
@$pb.TagNumber(1)
set encryptedData($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasEncryptedData() => $_has(0);
@$pb.TagNumber(1)
void clearEncryptedData() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get iv => $_getN(1);
@$pb.TagNumber(2)
set iv($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasIv() => $_has(1);
@$pb.TagNumber(2)
void clearIv() => $_clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get mac => $_getN(2);
@$pb.TagNumber(3)
set mac($core.List<$core.int> value) => $_setBytes(2, value);
@$pb.TagNumber(3)
$core.bool hasMac() => $_has(2);
@$pb.TagNumber(3)
void clearMac() => $_clearField(3);
}
class TrustedFriendShare_User extends $pb.GeneratedMessage {
factory TrustedFriendShare_User({
$fixnum.Int64? userId,
$core.String? displayName,
$core.List<$core.int>? avatar,
}) {
final result = create();
if (userId != null) result.userId = userId;
if (displayName != null) result.displayName = displayName;
if (avatar != null) result.avatar = avatar;
return result;
}
TrustedFriendShare_User._();
factory TrustedFriendShare_User.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory TrustedFriendShare_User.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'TrustedFriendShare.User',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'userId')
..aOS(2, _omitFieldNames ? '' : 'displayName')
..a<$core.List<$core.int>>(
3, _omitFieldNames ? '' : 'avatar', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
TrustedFriendShare_User clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
TrustedFriendShare_User copyWith(
void Function(TrustedFriendShare_User) updates) =>
super.copyWith((message) => updates(message as TrustedFriendShare_User))
as TrustedFriendShare_User;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static TrustedFriendShare_User create() => TrustedFriendShare_User._();
@$core.override
TrustedFriendShare_User createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static TrustedFriendShare_User getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<TrustedFriendShare_User>(create);
static TrustedFriendShare_User? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get userId => $_getI64(0);
@$pb.TagNumber(1)
set userId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasUserId() => $_has(0);
@$pb.TagNumber(1)
void clearUserId() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get displayName => $_getSZ(1);
@$pb.TagNumber(2)
set displayName($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasDisplayName() => $_has(1);
@$pb.TagNumber(2)
void clearDisplayName() => $_clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get avatar => $_getN(2);
@$pb.TagNumber(3)
set avatar($core.List<$core.int> value) => $_setBytes(2, value);
@$pb.TagNumber(3)
$core.bool hasAvatar() => $_has(2);
@$pb.TagNumber(3)
void clearAvatar() => $_clearField(3);
}
/// Send from the trusted friend to
/// This is encrypted with the received public key.
class TrustedFriendShare extends $pb.GeneratedMessage {
factory TrustedFriendShare({
TrustedFriendShare_User? trustedFriend,
TrustedFriendShare_User? shareUser,
$core.int? threshold,
$core.List<$core.int>? sharedSecretData,
}) {
final result = create();
if (trustedFriend != null) result.trustedFriend = trustedFriend;
if (shareUser != null) result.shareUser = shareUser;
if (threshold != null) result.threshold = threshold;
if (sharedSecretData != null) result.sharedSecretData = sharedSecretData;
return result;
}
TrustedFriendShare._();
factory TrustedFriendShare.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory TrustedFriendShare.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'TrustedFriendShare',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..aOM<TrustedFriendShare_User>(1, _omitFieldNames ? '' : 'trustedFriend',
subBuilder: TrustedFriendShare_User.create)
..aOM<TrustedFriendShare_User>(2, _omitFieldNames ? '' : 'shareUser',
subBuilder: TrustedFriendShare_User.create)
..aI(3, _omitFieldNames ? '' : 'threshold')
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'sharedSecretData', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
TrustedFriendShare clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
TrustedFriendShare copyWith(void Function(TrustedFriendShare) updates) =>
super.copyWith((message) => updates(message as TrustedFriendShare))
as TrustedFriendShare;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static TrustedFriendShare create() => TrustedFriendShare._();
@$core.override
TrustedFriendShare createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static TrustedFriendShare getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<TrustedFriendShare>(create);
static TrustedFriendShare? _defaultInstance;
/// This allows to display the user which user has send him his recovery data.
@$pb.TagNumber(1)
TrustedFriendShare_User get trustedFriend => $_getN(0);
@$pb.TagNumber(1)
set trustedFriend(TrustedFriendShare_User value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasTrustedFriend() => $_has(0);
@$pb.TagNumber(1)
void clearTrustedFriend() => $_clearField(1);
@$pb.TagNumber(1)
TrustedFriendShare_User ensureTrustedFriend() => $_ensure(0);
/// This allows to display the userdata, showing that he is recovering the correct person.
@$pb.TagNumber(2)
TrustedFriendShare_User get shareUser => $_getN(1);
@$pb.TagNumber(2)
set shareUser(TrustedFriendShare_User value) => $_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasShareUser() => $_has(1);
@$pb.TagNumber(2)
void clearShareUser() => $_clearField(2);
@$pb.TagNumber(2)
TrustedFriendShare_User ensureShareUser() => $_ensure(1);
/// The minimum threshold required to decrypte the shares.
@$pb.TagNumber(3)
$core.int get threshold => $_getIZ(2);
@$pb.TagNumber(3)
set threshold($core.int value) => $_setSignedInt32(2, value);
@$pb.TagNumber(3)
$core.bool hasThreshold() => $_has(2);
@$pb.TagNumber(3)
void clearThreshold() => $_clearField(3);
/// The actual share which will become: SharedSecretData
@$pb.TagNumber(4)
$core.List<$core.int> get sharedSecretData => $_getN(3);
@$pb.TagNumber(4)
set sharedSecretData($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasSharedSecretData() => $_has(3);
@$pb.TagNumber(4)
void clearSharedSecretData() => $_clearField(4);
}
class SharedSecretData_SecondFactorPin extends $pb.GeneratedMessage {
factory SharedSecretData_SecondFactorPin({
$core.List<$core.int>? unlockToken,
$core.List<$core.int>? pinSeed,
}) {
final result = create();
if (unlockToken != null) result.unlockToken = unlockToken;
if (pinSeed != null) result.pinSeed = pinSeed;
return result;
}
SharedSecretData_SecondFactorPin._();
factory SharedSecretData_SecondFactorPin.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory SharedSecretData_SecondFactorPin.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'SharedSecretData.SecondFactorPin',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'unlockToken', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'pinSeed', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SharedSecretData_SecondFactorPin clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SharedSecretData_SecondFactorPin copyWith(
void Function(SharedSecretData_SecondFactorPin) updates) =>
super.copyWith(
(message) => updates(message as SharedSecretData_SecondFactorPin))
as SharedSecretData_SecondFactorPin;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static SharedSecretData_SecondFactorPin create() =>
SharedSecretData_SecondFactorPin._();
@$core.override
SharedSecretData_SecondFactorPin createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static SharedSecretData_SecondFactorPin getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<SharedSecretData_SecondFactorPin>(
create);
static SharedSecretData_SecondFactorPin? _defaultInstance;
/// Required to try the PIN to get the share from the server.
/// This prevents that someone else can lock the pin, as the server only
/// allows 3 tries then after 1 day again 3 tries until the key is deleted.
@$pb.TagNumber(1)
$core.List<$core.int> get unlockToken => $_getN(0);
@$pb.TagNumber(1)
set unlockToken($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasUnlockToken() => $_has(0);
@$pb.TagNumber(1)
void clearUnlockToken() => $_clearField(1);
/// This never is send to the server but used to hash the pin before sending it to the server.
/// This prevents that the server every knows the shot 4-diget PIN.
@$pb.TagNumber(2)
$core.List<$core.int> get pinSeed => $_getN(1);
@$pb.TagNumber(2)
set pinSeed($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasPinSeed() => $_has(1);
@$pb.TagNumber(2)
void clearPinSeed() => $_clearField(2);
}
class SharedSecretData_SecondFactorMail extends $pb.GeneratedMessage {
factory SharedSecretData_SecondFactorMail() => create();
SharedSecretData_SecondFactorMail._();
factory SharedSecretData_SecondFactorMail.fromBuffer(
$core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory SharedSecretData_SecondFactorMail.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'SharedSecretData.SecondFactorMail',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SharedSecretData_SecondFactorMail clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SharedSecretData_SecondFactorMail copyWith(
void Function(SharedSecretData_SecondFactorMail) updates) =>
super.copyWith((message) =>
updates(message as SharedSecretData_SecondFactorMail))
as SharedSecretData_SecondFactorMail;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static SharedSecretData_SecondFactorMail create() =>
SharedSecretData_SecondFactorMail._();
@$core.override
SharedSecretData_SecondFactorMail createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static SharedSecretData_SecondFactorMail getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<SharedSecretData_SecondFactorMail>(
create);
static SharedSecretData_SecondFactorMail? _defaultInstance;
}
/// After received all shares this is decrypted by the user restoring its own
class SharedSecretData extends $pb.GeneratedMessage {
factory SharedSecretData({
RecoveryData? recoveryData,
SharedSecretData_SecondFactorMail? secondFactorMail,
SharedSecretData_SecondFactorPin? secondFactorPin,
$core.List<$core.int>? recoveryDataEncrypted,
}) {
final result = create();
if (recoveryData != null) result.recoveryData = recoveryData;
if (secondFactorMail != null) result.secondFactorMail = secondFactorMail;
if (secondFactorPin != null) result.secondFactorPin = secondFactorPin;
if (recoveryDataEncrypted != null)
result.recoveryDataEncrypted = recoveryDataEncrypted;
return result;
}
SharedSecretData._();
factory SharedSecretData.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory SharedSecretData.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'SharedSecretData',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..aOM<RecoveryData>(1, _omitFieldNames ? '' : 'recoveryData',
subBuilder: RecoveryData.create)
..aOM<SharedSecretData_SecondFactorMail>(
2, _omitFieldNames ? '' : 'secondFactorMail',
subBuilder: SharedSecretData_SecondFactorMail.create)
..aOM<SharedSecretData_SecondFactorPin>(
3, _omitFieldNames ? '' : 'secondFactorPin',
subBuilder: SharedSecretData_SecondFactorPin.create)
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'recoveryDataEncrypted', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SharedSecretData clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
SharedSecretData copyWith(void Function(SharedSecretData) updates) =>
super.copyWith((message) => updates(message as SharedSecretData))
as SharedSecretData;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static SharedSecretData create() => SharedSecretData._();
@$core.override
SharedSecretData createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static SharedSecretData getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<SharedSecretData>(create);
static SharedSecretData? _defaultInstance;
/// No second factor was selected
@$pb.TagNumber(1)
RecoveryData get recoveryData => $_getN(0);
@$pb.TagNumber(1)
set recoveryData(RecoveryData value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasRecoveryData() => $_has(0);
@$pb.TagNumber(1)
void clearRecoveryData() => $_clearField(1);
@$pb.TagNumber(1)
RecoveryData ensureRecoveryData() => $_ensure(0);
/// Server has
@$pb.TagNumber(2)
SharedSecretData_SecondFactorMail get secondFactorMail => $_getN(1);
@$pb.TagNumber(2)
set secondFactorMail(SharedSecretData_SecondFactorMail value) =>
$_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasSecondFactorMail() => $_has(1);
@$pb.TagNumber(2)
void clearSecondFactorMail() => $_clearField(2);
@$pb.TagNumber(2)
SharedSecretData_SecondFactorMail ensureSecondFactorMail() => $_ensure(1);
@$pb.TagNumber(3)
SharedSecretData_SecondFactorPin get secondFactorPin => $_getN(2);
@$pb.TagNumber(3)
set secondFactorPin(SharedSecretData_SecondFactorPin value) =>
$_setField(3, value);
@$pb.TagNumber(3)
$core.bool hasSecondFactorPin() => $_has(2);
@$pb.TagNumber(3)
void clearSecondFactorPin() => $_clearField(3);
@$pb.TagNumber(3)
SharedSecretData_SecondFactorPin ensureSecondFactorPin() => $_ensure(2);
/// The recovery data in case a second factor was used
/// The decryption key is loaded from the server either using the PIN or the MAIL
@$pb.TagNumber(4)
$core.List<$core.int> get recoveryDataEncrypted => $_getN(3);
@$pb.TagNumber(4)
set recoveryDataEncrypted($core.List<$core.int> value) =>
$_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasRecoveryDataEncrypted() => $_has(3);
@$pb.TagNumber(4)
void clearRecoveryDataEncrypted() => $_clearField(4);
}
/// The data which is recovered at the end.
/// The backup_master_key allows to recover the actual backup uploaded in the background to the server.
/// In case the backup is not available any more the user can use its user_id and his private_key to requister as a new user.
class RecoveryData extends $pb.GeneratedMessage {
factory RecoveryData({
$fixnum.Int64? userId,
$core.List<$core.int>? keyManager,
}) {
final result = create();
if (userId != null) result.userId = userId;
if (keyManager != null) result.keyManager = keyManager;
return result;
}
RecoveryData._();
factory RecoveryData.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory RecoveryData.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'RecoveryData',
package: const $pb.PackageName(
_omitMessageNames ? '' : 'passwordless_recovery'),
createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'userId')
..a<$core.List<$core.int>>(
3, _omitFieldNames ? '' : 'keyManager', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
RecoveryData clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
RecoveryData copyWith(void Function(RecoveryData) updates) =>
super.copyWith((message) => updates(message as RecoveryData))
as RecoveryData;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static RecoveryData create() => RecoveryData._();
@$core.override
RecoveryData createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static RecoveryData getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<RecoveryData>(create);
static RecoveryData? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get userId => $_getI64(0);
@$pb.TagNumber(1)
set userId($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasUserId() => $_has(0);
@$pb.TagNumber(1)
void clearUserId() => $_clearField(1);
@$pb.TagNumber(3)
$core.List<$core.int> get keyManager => $_getN(1);
@$pb.TagNumber(3)
set keyManager($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(3)
$core.bool hasKeyManager() => $_has(1);
@$pb.TagNumber(3)
void clearKeyManager() => $_clearField(3);
}
const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');

View file

@ -0,0 +1,11 @@
// This is a generated file - do not edit.
//
// Generated from types.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports

View file

@ -0,0 +1,195 @@
// This is a generated file - do not edit.
//
// Generated from types.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
// ignore_for_file: unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use recoveryRequestDescriptor instead')
const RecoveryRequest$json = {
'1': 'RecoveryRequest',
'2': [
{'1': 'temp_id', '3': 1, '4': 1, '5': 3, '10': 'tempId'},
{'1': 'public_key', '3': 2, '4': 1, '5': 12, '10': 'publicKey'},
],
};
/// Descriptor for `RecoveryRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List recoveryRequestDescriptor = $convert.base64Decode(
'Cg9SZWNvdmVyeVJlcXVlc3QSFwoHdGVtcF9pZBgBIAEoA1IGdGVtcElkEh0KCnB1YmxpY19rZX'
'kYAiABKAxSCXB1YmxpY0tleQ==');
@$core.Deprecated('Use encryptedEnvelopeDescriptor instead')
const EncryptedEnvelope$json = {
'1': 'EncryptedEnvelope',
'2': [
{'1': 'encrypted_data', '3': 1, '4': 1, '5': 12, '10': 'encryptedData'},
{'1': 'iv', '3': 2, '4': 1, '5': 12, '10': 'iv'},
{'1': 'mac', '3': 3, '4': 1, '5': 12, '10': 'mac'},
],
};
/// Descriptor for `EncryptedEnvelope`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List encryptedEnvelopeDescriptor = $convert.base64Decode(
'ChFFbmNyeXB0ZWRFbnZlbG9wZRIlCg5lbmNyeXB0ZWRfZGF0YRgBIAEoDFINZW5jcnlwdGVkRG'
'F0YRIOCgJpdhgCIAEoDFICaXYSEAoDbWFjGAMgASgMUgNtYWM=');
@$core.Deprecated('Use trustedFriendShareDescriptor instead')
const TrustedFriendShare$json = {
'1': 'TrustedFriendShare',
'2': [
{
'1': 'trusted_friend',
'3': 1,
'4': 1,
'5': 11,
'6': '.passwordless_recovery.TrustedFriendShare.User',
'10': 'trustedFriend'
},
{
'1': 'share_user',
'3': 2,
'4': 1,
'5': 11,
'6': '.passwordless_recovery.TrustedFriendShare.User',
'10': 'shareUser'
},
{'1': 'threshold', '3': 3, '4': 1, '5': 5, '10': 'threshold'},
{
'1': 'shared_secret_data',
'3': 4,
'4': 1,
'5': 12,
'10': 'sharedSecretData'
},
],
'3': [TrustedFriendShare_User$json],
};
@$core.Deprecated('Use trustedFriendShareDescriptor instead')
const TrustedFriendShare_User$json = {
'1': 'User',
'2': [
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
{'1': 'display_name', '3': 2, '4': 1, '5': 9, '10': 'displayName'},
{'1': 'avatar', '3': 3, '4': 1, '5': 12, '10': 'avatar'},
],
};
/// Descriptor for `TrustedFriendShare`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List trustedFriendShareDescriptor = $convert.base64Decode(
'ChJUcnVzdGVkRnJpZW5kU2hhcmUSVQoOdHJ1c3RlZF9mcmllbmQYASABKAsyLi5wYXNzd29yZG'
'xlc3NfcmVjb3ZlcnkuVHJ1c3RlZEZyaWVuZFNoYXJlLlVzZXJSDXRydXN0ZWRGcmllbmQSTQoK'
'c2hhcmVfdXNlchgCIAEoCzIuLnBhc3N3b3JkbGVzc19yZWNvdmVyeS5UcnVzdGVkRnJpZW5kU2'
'hhcmUuVXNlclIJc2hhcmVVc2VyEhwKCXRocmVzaG9sZBgDIAEoBVIJdGhyZXNob2xkEiwKEnNo'
'YXJlZF9zZWNyZXRfZGF0YRgEIAEoDFIQc2hhcmVkU2VjcmV0RGF0YRpaCgRVc2VyEhcKB3VzZX'
'JfaWQYASABKANSBnVzZXJJZBIhCgxkaXNwbGF5X25hbWUYAiABKAlSC2Rpc3BsYXlOYW1lEhYK'
'BmF2YXRhchgDIAEoDFIGYXZhdGFy');
@$core.Deprecated('Use sharedSecretDataDescriptor instead')
const SharedSecretData$json = {
'1': 'SharedSecretData',
'2': [
{
'1': 'recovery_data',
'3': 1,
'4': 1,
'5': 11,
'6': '.passwordless_recovery.RecoveryData',
'9': 0,
'10': 'recoveryData',
'17': true
},
{
'1': 'second_factor_mail',
'3': 2,
'4': 1,
'5': 11,
'6': '.passwordless_recovery.SharedSecretData.SecondFactorMail',
'9': 1,
'10': 'secondFactorMail',
'17': true
},
{
'1': 'second_factor_pin',
'3': 3,
'4': 1,
'5': 11,
'6': '.passwordless_recovery.SharedSecretData.SecondFactorPin',
'9': 2,
'10': 'secondFactorPin',
'17': true
},
{
'1': 'recovery_data_encrypted',
'3': 4,
'4': 1,
'5': 12,
'9': 3,
'10': 'recoveryDataEncrypted',
'17': true
},
],
'3': [
SharedSecretData_SecondFactorPin$json,
SharedSecretData_SecondFactorMail$json
],
'8': [
{'1': '_recovery_data'},
{'1': '_second_factor_mail'},
{'1': '_second_factor_pin'},
{'1': '_recovery_data_encrypted'},
],
};
@$core.Deprecated('Use sharedSecretDataDescriptor instead')
const SharedSecretData_SecondFactorPin$json = {
'1': 'SecondFactorPin',
'2': [
{'1': 'unlock_token', '3': 1, '4': 1, '5': 12, '10': 'unlockToken'},
{'1': 'pin_seed', '3': 2, '4': 1, '5': 12, '10': 'pinSeed'},
],
};
@$core.Deprecated('Use sharedSecretDataDescriptor instead')
const SharedSecretData_SecondFactorMail$json = {
'1': 'SecondFactorMail',
};
/// Descriptor for `SharedSecretData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sharedSecretDataDescriptor = $convert.base64Decode(
'ChBTaGFyZWRTZWNyZXREYXRhEk0KDXJlY292ZXJ5X2RhdGEYASABKAsyIy5wYXNzd29yZGxlc3'
'NfcmVjb3ZlcnkuUmVjb3ZlcnlEYXRhSABSDHJlY292ZXJ5RGF0YYgBARJrChJzZWNvbmRfZmFj'
'dG9yX21haWwYAiABKAsyOC5wYXNzd29yZGxlc3NfcmVjb3ZlcnkuU2hhcmVkU2VjcmV0RGF0YS'
'5TZWNvbmRGYWN0b3JNYWlsSAFSEHNlY29uZEZhY3Rvck1haWyIAQESaAoRc2Vjb25kX2ZhY3Rv'
'cl9waW4YAyABKAsyNy5wYXNzd29yZGxlc3NfcmVjb3ZlcnkuU2hhcmVkU2VjcmV0RGF0YS5TZW'
'NvbmRGYWN0b3JQaW5IAlIPc2Vjb25kRmFjdG9yUGluiAEBEjsKF3JlY292ZXJ5X2RhdGFfZW5j'
'cnlwdGVkGAQgASgMSANSFXJlY292ZXJ5RGF0YUVuY3J5cHRlZIgBARpPCg9TZWNvbmRGYWN0b3'
'JQaW4SIQoMdW5sb2NrX3Rva2VuGAEgASgMUgt1bmxvY2tUb2tlbhIZCghwaW5fc2VlZBgCIAEo'
'DFIHcGluU2VlZBoSChBTZWNvbmRGYWN0b3JNYWlsQhAKDl9yZWNvdmVyeV9kYXRhQhUKE19zZW'
'NvbmRfZmFjdG9yX21haWxCFAoSX3NlY29uZF9mYWN0b3JfcGluQhoKGF9yZWNvdmVyeV9kYXRh'
'X2VuY3J5cHRlZA==');
@$core.Deprecated('Use recoveryDataDescriptor instead')
const RecoveryData$json = {
'1': 'RecoveryData',
'2': [
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
{'1': 'key_manager', '3': 3, '4': 1, '5': 12, '10': 'keyManager'},
],
};
/// Descriptor for `RecoveryData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List recoveryDataDescriptor = $convert.base64Decode(
'CgxSZWNvdmVyeURhdGESFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEh8KC2tleV9tYW5hZ2VyGA'
'MgASgMUgprZXlNYW5hZ2Vy');

View file

@ -37,6 +37,7 @@ message EncryptedContent {
/// This can be added, so the receiver can check weather he is up to date with the current profile /// This can be added, so the receiver can check weather he is up to date with the current profile
optional int64 sender_profile_counter = 4; optional int64 sender_profile_counter = 4;
optional bytes sender_user_discovery_version = 21; optional bytes sender_user_discovery_version = 21;
optional bool ask_for_friend_promotions = 25;
optional MessageUpdate message_update = 5; optional MessageUpdate message_update = 5;
optional Media media = 6; optional Media media = 6;
@ -57,12 +58,15 @@ message EncryptedContent {
optional UserDiscoveryRequest user_discovery_request = 22; optional UserDiscoveryRequest user_discovery_request = 22;
optional UserDiscoveryUpdate user_discovery_update = 23; optional UserDiscoveryUpdate user_discovery_update = 23;
optional KeyVerificationProof key_verification_proof = 24; optional KeyVerificationProof key_verification_proof = 24;
optional PasswordLessRecovery passwordless_recovery = 26;
optional PasswordLessRecoveryHeartbeat passwordless_recovery_heartbeat = 27;
message ErrorMessages { message ErrorMessages {
enum Type { enum Type {
ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD = 0; ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD = 0;
UNKNOWN_MESSAGE_TYPE = 2; UNKNOWN_MESSAGE_TYPE = 2;
SESSION_OUT_OF_SYNC = 3; SESSION_OUT_OF_SYNC = 3;
GROUP_NOT_FOUND_OR_NOT_A_MEMBER = 4;
} }
Type type = 1; Type type = 1;
string related_receipt_id = 2; string related_receipt_id = 2;
@ -70,8 +74,9 @@ message EncryptedContent {
message GroupCreate { message GroupCreate {
// key for the state stored on the server // key for the state stored on the server
bytes state_key = 3; bytes state_key = 3;
bytes group_public_key = 4; bytes group_public_key = 4;
optional string group_name = 5;
} }
message GroupJoin { message GroupJoin {
@ -79,9 +84,7 @@ message EncryptedContent {
bytes group_public_key = 1; bytes group_public_key = 1;
} }
message ResendGroupPublicKey { message ResendGroupPublicKey {}
}
message GroupUpdate { message GroupUpdate {
string group_action_type = 1; // GroupActionType.name string group_action_type = 1; // GroupActionType.name
@ -214,4 +217,14 @@ message EncryptedContent {
bytes calculated_mac = 1; bytes calculated_mac = 1;
} }
message PasswordLessRecovery {
optional bytes recoverySecretShare = 1;
bool delete = 2;
int64 threshold = 3;
}
message PasswordLessRecoveryHeartbeat {
bytes hash = 1;
}
} }

View file

@ -12,7 +12,7 @@ package passwordless_recovery;
// -> Using this phishing is harder, as the user has to manualy select the user to recovery // -> Using this phishing is harder, as the user has to manualy select the user to recovery
// -> The user who wants to recover his account does not need to remember her old username // -> The user who wants to recover his account does not need to remember her old username
message RecoveryRequest { message RecoveryRequest {
int64 temp_id = 1; string notification_id = 1;
bytes public_key = 2; bytes public_key = 2;
} }
@ -36,8 +36,8 @@ message TrustedFriendShare {
// The minimum threshold required to decrypte the shares. // The minimum threshold required to decrypte the shares.
int32 threshold = 3; int32 threshold = 3;
// The actual share which will become: SecretSharedDate // The actual share which will become: SharedSecretData
bytes share = 4; bytes shared_secret_data = 4;
message User { message User {
int64 user_id = 1; int64 user_id = 1;
@ -46,41 +46,24 @@ message TrustedFriendShare {
} }
} }
// After received all shares this is decrypted by the user restoring its own // The user identity keys. Can be used to restore the data backup.
message SecretSharedDate {
// No second factor was selected
optional RecoveryData recovery_data = 1;
// Server has
optional SecondFactorMail second_factor_mail = 2;
optional SecondFactorPin second_factor_pin = 3;
// The recovery data in case a second factor was used
// The decryption key is loaded from the server either using the PIN or the MAIL
optional bytes recovery_data_encrypted = 4;
message SecondFactorPin {
// Required to try the PIN to get the share from the server.
// This prevents that someone else can lock the pin, as the server only
// allows 3 tries then after 1 day again 3 tries until the key is deleted.
bytes unlock_token = 1;
// This never is send to the server but used to hash the pin before sending it to the server.
// This prevents that the server every knows the shot 4-diget PIN.
bytes pin_seed = 2;
}
message SecondFactorMail {}
}
// The data which is recovered at the end.
// The backup_master_key allows to recover the actual backup uploaded in the background to the server.
// In case the backup is not available any more the user can use its user_id and his private_key to requister as a new user.
message RecoveryData { message RecoveryData {
int64 user_id = 1; int64 user_id = 1;
bytes private_key = 2; bytes key_manager = 3;
bytes backup_master_key = 3;
} }
// After received all shares this is decrypted by the user restoring its own
message SharedSecretData {
// The recovery data is encrypted in case a second factor was chosen.
bytes recovery_data = 1;
optional bytes encrypted_server_key_nonce = 3;
optional bytes pin_seed = 4;
optional bytes pin_unlock_token = 5;
optional string email_hint = 6;
}

View file

@ -13,7 +13,8 @@ import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart';
import 'package:twonly/src/visual/views/contact/contact.view.dart'; import 'package:twonly/src/visual/views/contact/contact.view.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';
import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart'; import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart';
import 'package:twonly/src/visual/views/onboarding/recover.view.dart'; import 'package:twonly/src/visual/views/onboarding/recover_password.view.dart';
import 'package:twonly/src/visual/views/onboarding/recover_passwordless.view.dart';
import 'package:twonly/src/visual/views/public_profile.view.dart'; import 'package:twonly/src/visual/views/public_profile.view.dart';
import 'package:twonly/src/visual/views/settings/account.view.dart'; import 'package:twonly/src/visual/views/settings/account.view.dart';
import 'package:twonly/src/visual/views/settings/appearance.view.dart'; import 'package:twonly/src/visual/views/settings/appearance.view.dart';
@ -39,7 +40,6 @@ import 'package:twonly/src/visual/views/settings/help/help.view.dart';
import 'package:twonly/src/visual/views/settings/notification.view.dart'; import 'package:twonly/src/visual/views/settings/notification.view.dart';
import 'package:twonly/src/visual/views/settings/privacy.view.dart'; import 'package:twonly/src/visual/views/settings/privacy.view.dart';
import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart';
import 'package:twonly/src/visual/views/settings/privacy/profile_selection.view.dart';
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart';
import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart'; import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart';
import 'package:twonly/src/visual/views/settings/profile/profile.view.dart'; import 'package:twonly/src/visual/views/settings/profile/profile.view.dart';
@ -58,6 +58,13 @@ final routerProvider = GoRouter(
path: Routes.home, path: Routes.home,
builder: (context, state) => const AppMainWidget(initialPage: 1), builder: (context, state) => const AppMainWidget(initialPage: 1),
), ),
GoRoute(
path: Routes.recoverPasswordless,
builder: (context, state) {
final token = state.extra as String?;
return RecoverPasswordless(initialEmailToken: token);
},
),
// Chats // Chats
GoRoute( GoRoute(
@ -135,7 +142,13 @@ final routerProvider = GoRouter(
GoRoute( GoRoute(
path: Routes.cameraQRScanner, path: Routes.cameraQRScanner,
builder: (context, state) { builder: (context, state) {
return const QrCodeScannerView(); final extra = state.extra as Map<String, dynamic>?;
final contact = extra?['contact'] as Contact?;
final openToVerify = extra?['openToVerify'] as bool? ?? false;
return QrCodeScannerView(
contact: contact,
openToVerify: openToVerify,
);
}, },
), ),
@ -206,10 +219,6 @@ final routerProvider = GoRouter(
path: 'user_discovery', path: 'user_discovery',
builder: (context, state) => const UserDiscoverySettingsView(), builder: (context, state) => const UserDiscoverySettingsView(),
), ),
GoRoute(
path: 'profile_selection',
builder: (context, state) => const ProfileSelectionSettingsView(),
),
], ],
), ),
GoRoute( GoRoute(
@ -240,7 +249,9 @@ final routerProvider = GoRouter(
routes: [ routes: [
GoRoute( GoRoute(
path: 'verifybadge', path: 'verifybadge',
builder: (context, state) => const VerificationBadeFaqView(), builder: (context, state) => VerificationBadeFaqView(
contact: state.extra as Contact?,
),
), ),
], ],
), ),

View file

@ -21,7 +21,8 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as server; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart'; import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/download.api.dart';
@ -33,6 +34,7 @@ import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart'; import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
@ -65,13 +67,15 @@ class ApiService {
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream; Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
final _connectionStateController = StreamController<bool>.broadcast(); final _connectionStateController = StreamController<bool>.broadcast();
Stream<bool> get onConnectionStateUpdated => _connectionStateController.stream; Stream<bool> get onConnectionStateUpdated =>
_connectionStateController.stream;
final _appOutdatedController = StreamController<void>.broadcast(); final _appOutdatedController = StreamController<void>.broadcast();
Stream<void> get onAppOutdated => _appOutdatedController.stream; Stream<void> get onAppOutdated => _appOutdatedController.stream;
final _newDeviceRegisteredController = StreamController<void>.broadcast(); final _newDeviceRegisteredController = StreamController<void>.broadcast();
Stream<void> get onNewDeviceRegistered => _newDeviceRegisteredController.stream; Stream<void> get onNewDeviceRegistered =>
_newDeviceRegisteredController.stream;
bool appIsOutdated = false; bool appIsOutdated = false;
bool isAuthenticated = false; bool isAuthenticated = false;
@ -80,7 +84,8 @@ class ApiService {
Timer? reconnectionTimer; Timer? reconnectionTimer;
int _reconnectionDelay = 5; int _reconnectionDelay = 5;
final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests = HashMap(); final HashMap<Int64, Completer<server.ServerToClient?>> _pendingRequests =
HashMap();
IOWebSocketChannel? _channel; IOWebSocketChannel? _channel;
// ignore: cancel_subscriptions // ignore: cancel_subscriptions
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription; StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
@ -112,7 +117,7 @@ class ApiService {
// Function is called after the user is authenticated at the server // Function is called after the user is authenticated at the server
Future<void> onAuthenticated() async { Future<void> onAuthenticated() async {
await initFCMAfterAuthenticated(); await FcmNotificationService.initFCMAfterAuthenticated();
_connectionStateController.add(true); _connectionStateController.add(true);
if (AppState.isInBackgroundTask) { if (AppState.isInBackgroundTask) {
@ -135,6 +140,7 @@ class ApiService {
unawaited(fetchGroupStatesForUnjoinedGroups()); unawaited(fetchGroupStatesForUnjoinedGroups());
unawaited(fetchMissingGroupPublicKey()); unawaited(fetchMissingGroupPublicKey());
unawaited(checkForDeletedUsernames()); unawaited(checkForDeletedUsernames());
unawaited(PasswordlessRecoveryService.performHeartbeat());
unawaited(UserDiscoveryService.checkForNewAnnouncedUsers()); unawaited(UserDiscoveryService.checkForNewAnnouncedUsers());
@ -418,7 +424,9 @@ class ApiService {
} }
if (res.error == ErrorCode.UserIdNotFound && contactId != null) { if (res.error == ErrorCode.UserIdNotFound && contactId != null) {
Log.warn('Contact deleted their account $contactId.'); Log.warn('Contact deleted their account $contactId.');
final contact = await twonlyDB.contactsDao.getContactByUserId(contactId).getSingleOrNull(); final contact = await twonlyDB.contactsDao
.getContactByUserId(contactId)
.getSingleOrNull();
if (contact != null) { if (contact != null) {
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(
contactId, contactId,
@ -483,7 +491,8 @@ class ApiService {
return true; return true;
} }
if (result.isError) { if (result.isError) {
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) { if (result.error != ErrorCode.AuthTokenNotValid &&
result.error != ErrorCode.ForegroundSessionConnected) {
Log.error( Log.error(
'got error while authenticating to the server: ${result.error}', 'got error while authenticating to the server: ${result.error}',
); );
@ -521,7 +530,8 @@ class ApiService {
return true; return true;
} }
if (result.isError) { if (result.isError) {
if (result.error != ErrorCode.AuthTokenNotValid && result.error != ErrorCode.ForegroundSessionConnected) { if (result.error != ErrorCode.AuthTokenNotValid &&
result.error != ErrorCode.ForegroundSessionConnected) {
Log.error( Log.error(
'got error while authenticating to the server: ${result.error}', 'got error while authenticating to the server: ${result.error}',
); );
@ -553,7 +563,8 @@ class ApiService {
return; return;
} }
final handshake = Handshake()..getAuthChallenge = Handshake_GetAuthChallenge(); final handshake = Handshake()
..getAuthChallenge = Handshake_GetAuthChallenge();
final req = createClientToServerFromHandshake(handshake); final req = createClientToServerFromHandshake(handshake);
final result = await sendRequestSync(req, authenticated: false); final result = await sendRequestSync(req, authenticated: false);
@ -618,7 +629,9 @@ class ApiService {
final register = Handshake_Register() final register = Handshake_Register()
..username = username ..username = username
..publicIdentityKey = (await signalStore.getIdentityKeyPair()).getPublicKey().serialize() ..publicIdentityKey = (await signalStore.getIdentityKeyPair())
.getPublicKey()
.serialize()
..registrationId = Int64(signalIdentity.registrationId) ..registrationId = Int64(signalIdentity.registrationId)
..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize() ..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize()
..signedPrekeySignature = signedPreKey.signature ..signedPrekeySignature = signedPreKey.signature
@ -673,11 +686,11 @@ class ApiService {
final req = createClientToServerFromHandshake(handshake); final req = createClientToServerFromHandshake(handshake);
final result = await sendRequestSync(req, authenticated: false); final result = await sendRequestSync(req, authenticated: false);
if (result.isError) { if (result.isError) {
Log.error('could not request proof of work params', result); Log.error('could not request proof of work params', error: result);
if (result.error == ErrorCode.RegistrationDisabled) { if (result.error == ErrorCode.RegistrationDisabled) {
return (null, true); return (null, true);
} }
Log.error('could not request proof of work params', result); Log.error('could not request proof of work params', error: result);
return (null, false); return (null, false);
} }
return (result.value.proofOfWork as Response_ProofOfWork, false); return (result.value.proofOfWork as Response_ProofOfWork, false);
@ -747,6 +760,85 @@ class ApiService {
return sendRequestSync(req, contactId: userId.toInt()); return sendRequestSync(req, contactId: userId.toInt());
} }
Future<Result> registerPasswordLessRecovery(
List<int> encryptedServerKey,
List<int>? pinUnlockToken,
) async {
final req = createClientToServerFromApplicationData(
ApplicationData(
registerPasswordlessRecovery:
ApplicationData_RegisterPasswordLessRecovery(
encryptedServerKey: encryptedServerKey,
pinUnlockToken: pinUnlockToken,
),
),
);
return sendRequestSync(req);
}
Future<Result> getServerKeyForPasswordlessRecovery({
required int userId,
List<int>? encryptedServerKeyNone,
List<int>? pinUnlockToken,
List<int>? pinProtectionKey,
String? email,
}) async {
final get = Handshake_GetServerKeyForPasswordLessRecovery()
..userId = Int64(userId);
if (encryptedServerKeyNone != null) {
get.encryptedServerKeyNone = encryptedServerKeyNone;
}
if (pinUnlockToken != null) {
get.pinUnlockToken = pinUnlockToken;
}
if (pinProtectionKey != null) {
get.pinProtectionKey = pinProtectionKey;
}
if (email != null) {
get.email = email;
}
final handshake = Handshake()..getServerKeyForPasswordlessRecovery = get;
final req = createClientToServerFromHandshake(handshake);
return sendRequestSync(req, authenticated: false);
}
Future<Result> submitRecoveryShare({
required String notificationId,
required List<int> encryptedMessage,
}) async {
final req = createClientToServerFromApplicationData(
ApplicationData(
passwordlessNotification: ApplicationData_PasswordlessNotification(
notificationId: notificationId,
encryptedMessage: encryptedMessage,
),
),
);
return sendRequestSync(req);
}
Future<Result> registerPasswordlessNotification({
required String notificationId,
required List<int> downloadAuthToken,
required String langCode,
required String? googleFcm,
}) async {
final registerNotif = Handshake_RegisterPasswordlessNotification()
..notificationId = notificationId
..downloadAuthToken = downloadAuthToken
..langCode = langCode;
if (googleFcm != null) {
registerNotif.googleFcm = googleFcm;
}
final handshake = Handshake()
..registerPasswordlessNotification = registerNotif;
final req = createClientToServerFromHandshake(handshake);
return sendRequestSync(req, authenticated: false);
}
Future<Result> addAdditionalUser(Int64 userId) async { Future<Result> addAdditionalUser(Int64 userId) async {
final get = ApplicationData_AddAdditionalUser()..userId = userId; final get = ApplicationData_AddAdditionalUser()..userId = userId;
final appData = ApplicationData()..addAdditionalUser = get; final appData = ApplicationData()..addAdditionalUser = get;
@ -859,4 +951,30 @@ class ApiService {
final req = createClientToServerFromApplicationData(appData); final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req, contactId: target); return sendRequestSync(req, contactId: target);
} }
/// Polls the server for new passwordless recovery notification messages.
/// [alreadyReceivedIds] prevents the server from sending duplicates.
Future<server.Response_PasswordlessNotificationMessages?> checkForPasswordlessNotification({
required String notificationId,
required List<int> downloadAuthToken,
List<Int64>? alreadyReceivedIds,
}) async {
final check = Handshake_CheckForPasswordlessNotification()
..notificationId = notificationId
..downloadAuthToken = downloadAuthToken;
if (alreadyReceivedIds != null && alreadyReceivedIds.isNotEmpty) {
check.alreadyReceivedMessageIds.addAll(alreadyReceivedIds);
}
final handshake = Handshake()..checkForPasswordlessNotification = check;
final req = createClientToServerFromHandshake(handshake);
final res = await sendRequestSync(req, authenticated: false);
if (res.isSuccess) {
final ok = res.value as server.Response_Ok;
if (ok.hasPasswordlessNotificationMessages()) {
return ok.passwordlessNotificationMessages;
}
}
return null;
}
} }

View file

@ -2,8 +2,11 @@ import 'package:clock/clock.dart' show clock;
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'
as pb_data;
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleAdditionalDataMessage( Future<void> handleAdditionalDataMessage(
@ -28,6 +31,25 @@ Future<void> handleAdditionalDataMessage(
return; return;
} }
try {
final additionalData = pb_data.AdditionalMessageData.fromBuffer(
message.additionalMessageData,
);
if (additionalData.type == pb_data.AdditionalMessageData_Type.CONTACTS) {
for (final sharedContact in additionalData.contacts) {
await KeyVerificationService.verifySharedContact(
contactId: sharedContact.userId.toInt(),
sharedPublicIdentityKey: sharedContact.publicIdentityKey,
senderId: fromUserId,
);
}
}
} catch (e) {
Log.error(
'Failed to parse additional message data or verify shared contacts: $e',
);
}
final msg = await twonlyDB.messagesDao.insertMessage( final msg = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
messageId: Value(message.senderMessageId), messageId: Value(message.senderMessageId),
@ -46,6 +68,8 @@ Future<void> handleAdditionalDataMessage(
fromTimestamp(message.timestamp), fromTimestamp(message.timestamp),
); );
if (msg != null) { if (msg != null) {
Log.info('[$receiptId] Inserted a new text message with ID: ${msg.messageId}'); Log.info(
'[$receiptId] Inserted a new text message with ID: ${msg.messageId}',
);
} }
} }

View file

@ -1,16 +1,23 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:fixnum/fixnum.dart' show Int64;
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
show IdentityKeyPair;
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/services/api/messages.api.dart'
show sendCipherText, tryToSendCompleteMessage;
import 'package:twonly/src/services/group.service.dart' show fetchGroupState;
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleErrorMessage( Future<void> handleErrorMessage(
int fromUserId, int fromUserId,
EncryptedContent_ErrorMessages error, EncryptedContent_ErrorMessages error,
String receiptId, String receiptId, {
) async { String? groupId,
Log.error('[$receiptId] Got error from $fromUserId: $error'); }) async {
Log.warn('[$receiptId] Got error from $fromUserId: $error');
switch (error.type) { switch (error.type) {
case EncryptedContent_ErrorMessages_Type case EncryptedContent_ErrorMessages_Type
@ -29,6 +36,59 @@ Future<void> handleErrorMessage(
); );
case EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC: case EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC:
break; // The other user initiated a new signal session, so ignore the error in this case, as the new session works... break; // The other user initiated a new signal session, so ignore the error in this case, as the new session works...
case EncryptedContent_ErrorMessages_Type.GROUP_NOT_FOUND_OR_NOT_A_MEMBER:
if (groupId == null) {
Log.warn(
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but groupId is null.',
);
return;
}
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null) {
Log.warn(
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but group $groupId is not found in database.',
);
return;
}
// Update group state from the server to ensure the user is still part of the group...
final updatedState = await fetchGroupState(group);
if (updatedState != null) {
final (_, state) = updatedState;
final isStillMember = state.memberIds.contains(Int64(fromUserId));
if (isStillMember) {
final keyPair = IdentityKeyPair.fromSerialized(
group.myGroupPrivateKey!,
);
await sendCipherText(
fromUserId,
EncryptedContent(
groupId: groupId,
groupCreate: EncryptedContent_GroupCreate(
stateKey: group.stateEncryptionKey,
groupPublicKey: keyPair.getPublicKey().serialize(),
),
),
);
}
final r = await twonlyDB.receiptsDao.getReceiptById(
error.relatedReceiptId,
);
if (r != null) {
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
fromUserId,
error.relatedReceiptId,
ReceiptsCompanion(
markForRetry: Value(clock.now()),
retryCount: Value(r.retryCount + 1),
),
);
}
// then resend: error.relatedReceiptId
await tryToSendCompleteMessage(
receiptId: error.relatedReceiptId,
blocking: false,
);
} else {}
// ignore: no_default_cases // ignore: no_default_cases
default: default:
break; break;

View file

@ -17,7 +17,9 @@ Future<void> handleGroupCreate(
EncryptedContent_GroupCreate newGroup, EncryptedContent_GroupCreate newGroup,
String receiptId, String receiptId,
) async { ) async {
final user = await twonlyDB.contactsDao.getContactByUserId(fromUserId).getSingleOrNull(); final user = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (user == null) { if (user == null) {
// Only contacts can invite other contacts, so this can (via the UI) not happen. // Only contacts can invite other contacts, so this can (via the UI) not happen.
Log.error( Log.error(
@ -43,11 +45,26 @@ Future<void> handleGroupCreate(
stateVersionId: const Value(0), stateVersionId: const Value(0),
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
myGroupPrivateKey: Value(myGroupKey.serialize()), myGroupPrivateKey: Value(myGroupKey.serialize()),
groupName: const Value(''), groupName: Value(newGroup.hasGroupName() ? newGroup.groupName : ''),
joinedGroup: const Value(false), joinedGroup: const Value(false),
), ),
); );
} else { } else {
// In this case make a group state update and check if the fromUserId is still a admin. otherwise return with an log error message
final updatedState = await fetchGroupState(group);
if (updatedState == null) {
Log.error(
'[$receiptId] Received group invite/create for $groupId, but failed to fetch group state from server.',
);
return;
}
final (_, state) = updatedState;
if (!state.adminIds.any((id) => id.toInt() == fromUserId)) {
Log.error(
'[$receiptId] Received group invite/create for $groupId from $fromUserId, but they are not an admin of this group.',
);
return;
}
// User was already in the group, so update leftGroup back to false // User was already in the group, so update leftGroup back to false
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
groupId, groupId,
@ -55,7 +72,6 @@ Future<void> handleGroupCreate(
stateVersionId: const Value(0), stateVersionId: const Value(0),
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
myGroupPrivateKey: Value(myGroupKey.serialize()), myGroupPrivateKey: Value(myGroupKey.serialize()),
groupName: const Value(''),
joinedGroup: const Value(false), joinedGroup: const Value(false),
leftGroup: const Value(false), leftGroup: const Value(false),
deletedContent: const Value(false), deletedContent: const Value(false),
@ -79,18 +95,8 @@ Future<void> handleGroupCreate(
), ),
); );
await twonlyDB.groupsDao.insertOrUpdateGroupMember( // Load group members from the server as this is the single source of truth.
GroupMembersCompanion( // This can be done in the background, so the WebSocket message can be ACKed.
groupId: Value(groupId),
contactId: Value(fromUserId),
memberState: const Value(
MemberState.admin, // is the group creator, so must be admin...
),
groupPublicKey: Value(Uint8List.fromList(newGroup.groupPublicKey)),
),
);
// can be done in the background -> websocket message can be ACK
unawaited(fetchGroupStatesForUnjoinedGroups()); unawaited(fetchGroupStatesForUnjoinedGroups());
await sendCipherTextToGroup( await sendCipherTextToGroup(
@ -113,7 +119,9 @@ Future<void> handleGroupUpdate(
final actionType = groupActionTypeFromString(update.groupActionType); final actionType = groupActionTypeFromString(update.groupActionType);
if (actionType == null) { if (actionType == null) {
Log.error('[$receiptId] Group action ${update.groupActionType} is unknown ignoring.'); Log.error(
'[$receiptId] Group action ${update.groupActionType} is unknown ignoring.',
);
return; return;
} }

View file

@ -14,7 +14,7 @@ import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleMedia( Future<bool> handleMedia(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_Media media, EncryptedContent_Media media,
@ -36,7 +36,7 @@ Future<void> handleMedia(
Log.warn( Log.warn(
'[$receiptId] Got reupload for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}', '[$receiptId] Got reupload for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
); );
return; return false;
} }
// in case there was already a downloaded file delete it... // in case there was already a downloaded file delete it...
@ -64,7 +64,7 @@ Future<void> handleMedia(
unawaited(startDownloadMedia(mediaFile, false)); unawaited(startDownloadMedia(mediaFile, false));
} }
return; return true;
case EncryptedContent_Media_Type.IMAGE: case EncryptedContent_Media_Type.IMAGE:
mediaType = MediaType.image; mediaType = MediaType.image;
case EncryptedContent_Media_Type.VIDEO: case EncryptedContent_Media_Type.VIDEO:
@ -85,13 +85,13 @@ Future<void> handleMedia(
Log.warn( Log.warn(
'[$receiptId] $fromUserId tried to modify the message from ${messageTmp.senderId}.', '[$receiptId] $fromUserId tried to modify the message from ${messageTmp.senderId}.',
); );
return; return false;
} }
if (messageTmp.mediaId == null) { if (messageTmp.mediaId == null) {
Log.warn( Log.warn(
'[$receiptId] This message already exit without a mediaId. Message is dropped.', '[$receiptId] This message already exit without a mediaId. Message is dropped.',
); );
return; return false;
} }
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
messageTmp.mediaId!, messageTmp.mediaId!,
@ -100,7 +100,7 @@ Future<void> handleMedia(
Log.warn( Log.warn(
'[$receiptId] This message and media file already exit and was not requested again. Dropping it.', '[$receiptId] This message and media file already exit and was not requested again. Dropping it.',
); );
return; return false;
} }
if (mediaFile != null) { if (mediaFile != null) {
@ -145,9 +145,13 @@ Future<void> handleMedia(
if (mediaFile == null) { if (mediaFile == null) {
Log.error('[$receiptId] Could not insert media file into database'); Log.error('[$receiptId] Could not insert media file into database');
return; return false;
} }
Log.info(
'[$receiptId] Inserting media message: messageId=${media.senderMessageId}, mediaId=${mediaFile!.mediaId}',
);
message = await twonlyDB.messagesDao.insertMessage( message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
messageId: Value(media.senderMessageId), messageId: Value(media.senderMessageId),
@ -186,6 +190,7 @@ Future<void> handleMedia(
); );
unawaited(startDownloadMedia(mediaFile!, false)); unawaited(startDownloadMedia(mediaFile!, false));
return true;
} else { } else {
if (mediaFile == null && message == null) { if (mediaFile == null && message == null) {
Log.error( Log.error(
@ -200,6 +205,7 @@ Future<void> handleMedia(
'[$receiptId] Could not insert new message as the message is empty.', '[$receiptId] Could not insert new message as the message is empty.',
); );
} }
return false;
} }
} }

View file

@ -7,7 +7,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleTextMessage( Future<bool> handleTextMessage(
int fromUserId, int fromUserId,
String groupId, String groupId,
EncryptedContent_TextMessage textMessage, EncryptedContent_TextMessage textMessage,
@ -26,7 +26,7 @@ Future<void> handleTextMessage(
Log.warn( Log.warn(
'[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.', '[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
); );
return; return false;
} }
final message = await twonlyDB.messagesDao.insertMessage( final message = await twonlyDB.messagesDao.insertMessage(
@ -50,4 +50,5 @@ Future<void> handleTextMessage(
if (message != null) { if (message != null) {
Log.info('[$receiptId] Inserted a new text message with ID: ${message.messageId}'); Log.info('[$receiptId] Inserted a new text message with ID: ${message.messageId}');
} }
return message != null;
} }

View file

@ -143,8 +143,12 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
failed = false; failed = false;
} else { } else {
failed = true; failed = true;
Log.warn(
'[$mediaId] Got invalid response status code: ${update.responseStatusCode}',
);
Log.error( Log.error(
'Got invalid response status code: ${update.responseStatusCode}', 'Got invalid response status code: ${update.responseStatusCode}',
onlyIfSentryEnabled: true,
); );
} }
} else { } else {
@ -227,7 +231,7 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
try { try {
await downloadFileFast(media, apiUrl, mediaService.encryptedPath); await downloadFileFast(media, apiUrl, mediaService.encryptedPath);
} catch (e) { } catch (e) {
Log.error('Fast download failed: $e'); Log.warn('Fast download failed: $e');
await FileDownloader().enqueue(task); await FileDownloader().enqueue(task);
} }
} catch (e) { } catch (e) {
@ -251,9 +255,13 @@ Future<void> downloadFileFast(
return; return;
} else { } else {
if (response.statusCode == 404 || response.statusCode == 403) { if (response.statusCode == 404 || response.statusCode == 403) {
Log.error( Log.warn(
'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again', 'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again',
); );
Log.error(
'Got ${response.statusCode} from server for media ID.',
onlyIfSentryEnabled: true,
);
// Message was deleted from the server. Requesting it again from the sender to upload it again... // Message was deleted from the server. Requesting it again from the sender to upload it again...
await requestMediaReupload(media.mediaId); await requestMediaReupload(media.mediaId);
return; return;
@ -264,6 +272,13 @@ Future<void> downloadFileFast(
} }
Future<void> requestMediaReupload(String mediaId) async { Future<void> requestMediaReupload(String mediaId) async {
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.reuploadRequested),
),
);
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
for (final message in messages) { for (final message in messages) {
@ -277,12 +292,6 @@ Future<void> requestMediaReupload(String mediaId) async {
), ),
), ),
); );
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.reuploadRequested),
),
);
} }
} }
@ -293,7 +302,7 @@ Future<void> handleEncryptedFile(String mediaId) async {
action: () async { action: () async {
final mediaService = await MediaFileService.fromMediaId(mediaId); final mediaService = await MediaFileService.fromMediaId(mediaId);
if (mediaService == null) { if (mediaService == null) {
Log.error('Media file not found in database.'); Log.warn('[$mediaId] Media file not found in database.');
return; return;
} }

View file

@ -80,9 +80,13 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
await markUploadAsSuccessful(media); await markUploadAsSuccessful(media);
return; return;
} }
Log.error( Log.warn(
'Got HTTP error ${update.responseStatusCode} for $mediaId', 'Got HTTP error ${update.responseStatusCode} for $mediaId',
); );
Log.error(
'Got HTTP error ${update.responseStatusCode} for media.',
onlyIfSentryEnabled: true,
);
} }
if (update.status == TaskStatus.notFound) { if (update.status == TaskStatus.notFound) {
@ -118,9 +122,13 @@ Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) { update.status == TaskStatus.canceled) {
Log.error( Log.warn(
'Background upload failed for $mediaId with status ${update.status} and ${update.responseStatusCode}. ', 'Background upload failed for $mediaId with status ${update.status} and ${update.responseStatusCode}. ',
); );
Log.error(
'Background upload failed with status ${update.status} and ${update.responseStatusCode}.',
onlyIfSentryEnabled: true,
);
final mediaService = MediaFileService(media); final mediaService = MediaFileService(media);
// in case the media file is already uploaded to not reqtry // in case the media file is already uploaded to not reqtry

View file

@ -227,46 +227,54 @@ Future<void> finishStartedPreprocessing() async {
if (!service.originalPath.existsSync() && if (!service.originalPath.existsSync() &&
!service.uploadRequestPath.existsSync()) { !service.uploadRequestPath.existsSync()) {
if (service.storedPath.existsSync()) { if (service.storedPath.existsSync()) {
// media files was just stored.. // media file was stored, we can recover tempPath from storedPath and upload it.
continue; try {
} if (!service.tempPath.existsSync()) {
if (mediaFile.reuploadRequestedBy != null) { service.storedPath.copySync(service.tempPath.path);
Log.warn( }
'Reupload requested for ${mediaFile.mediaId} but files are missing. Cancelling reupload but keeping record.', } catch (e) {
); Log.error('Error recovering tempPath from storedPath: $e');
await twonlyDB.mediaFilesDao.updateMedia( continue;
mediaFile.mediaId, }
const MediaFilesCompanion(
uploadState: Value(UploadState.uploaded),
reuploadRequestedBy: Value(null),
),
);
continue;
}
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
mediaFile.mediaId,
);
if (messages.isEmpty) {
Log.info(
'Deleted media files ${mediaFile.mediaId} as originalPath and uploadRequestPath both do not exists and no messages reference it.',
);
// the file does not exists anymore and no messages reference it.
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
} else { } else {
Log.warn( if (mediaFile.reuploadRequestedBy != null) {
'Media files ${mediaFile.mediaId} missing but messages still reference it. Keeping record to avoid broken chat history.', Log.warn(
); 'Reupload requested for ${mediaFile.mediaId} but files are missing. Cancelling reupload but keeping record.',
// Just mark as uploaded to stop preprocessing attempts );
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploaded),
reuploadRequestedBy: Value(null),
),
);
continue;
}
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
mediaFile.mediaId, mediaFile.mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploaded),
),
); );
if (messages.isEmpty) {
Log.info(
'Deleted media files ${mediaFile.mediaId} as originalPath and uploadRequestPath both do not exists and no messages reference it.',
);
// the file does not exists anymore and no messages reference it.
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
} else {
Log.warn(
'Media files ${mediaFile.mediaId} missing but messages still reference it. Keeping record to avoid broken chat history.',
);
// Just mark as uploaded to stop preprocessing attempts
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploaded),
),
);
}
continue;
} }
continue;
} }
Log.info( Log.info(
'Finishing started preprocessing of ${mediaFile.mediaId} in state ${mediaFile.uploadState}.', 'Finishing started preprocessing of ${mediaFile.mediaId} in state ${mediaFile.uploadState}.',
@ -431,17 +439,45 @@ Future<void> _startBackgroundMediaUploadInternal(
// Refresh the media file state inside the mutex // Refresh the media file state inside the mutex
await mediaService.updateFromDB(); await mediaService.updateFromDB();
if (mediaService.mediaFile.uploadState == UploadState.uploading) {
await _checkAndRecoverMissingUploadRequest(mediaService);
}
if (mediaService.mediaFile.uploadState == UploadState.initialized || if (mediaService.mediaFile.uploadState == UploadState.initialized ||
mediaService.mediaFile.uploadState == UploadState.preprocessing) { mediaService.mediaFile.uploadState == UploadState.preprocessing) {
Log.info( Log.info(
'Hanlding media file ${mediaService.mediaFile.mediaId} in ${mediaService.mediaFile.uploadState}', 'Handling media file ${mediaService.mediaFile.mediaId} in ${mediaService.mediaFile.uploadState}',
); );
await mediaService.setUploadState(UploadState.preprocessing); await mediaService.setUploadState(UploadState.preprocessing);
if (!mediaService.tempPath.existsSync()) { if (!mediaService.tempPath.existsSync()) {
await mediaService.compressMedia(); if (mediaService.storedPath.existsSync()) {
try {
mediaService.storedPath.copySync(mediaService.tempPath.path);
} catch (e) {
Log.error('Error copying storedPath to tempPath: $e');
}
} else {
await mediaService.compressMedia();
}
if (!mediaService.tempPath.existsSync()) { if (!mediaService.tempPath.existsSync()) {
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
mediaService.mediaFile.mediaId,
);
if (messages.isEmpty) {
Log.warn(
'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path. Removing it from DB as files are not existent.',
);
await twonlyDB.mediaFilesDao.deleteMediaFile(
mediaService.mediaFile.mediaId,
);
} else {
Log.warn(
'Media files ${mediaService.mediaFile.mediaId} has no original, temp, or stored path, but messages still reference it. Marking as uploaded to stop retries.',
);
await mediaService.setUploadState(UploadState.uploaded);
}
return; return;
} }
} }
@ -640,7 +676,31 @@ Future<void> _createUploadRequest(MediaFileService media) async {
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes); await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
} }
Future<bool> _checkAndRecoverMissingUploadRequest(
MediaFileService media, {
bool triggerBackgroundUpload = false,
}) async {
if (!media.uploadRequestPath.existsSync()) {
Log.warn(
'UploadRequestPath for media ${media.mediaFile.mediaId} does not exist. Reverting to preprocessing.',
);
await media.setUploadState(UploadState.preprocessing);
if (triggerBackgroundUpload) {
unawaited(startBackgroundMediaUpload(media));
}
return true;
}
return false;
}
Future<void> _uploadUploadRequest(MediaFileService media) async { Future<void> _uploadUploadRequest(MediaFileService media) async {
if (await _checkAndRecoverMissingUploadRequest(
media,
triggerBackgroundUpload: true,
)) {
return;
}
final currentMedia = await twonlyDB.mediaFilesDao.getMediaFileById( final currentMedia = await twonlyDB.mediaFilesDao.getMediaFileById(
media.mediaFile.mediaId, media.mediaFile.mediaId,
); );
@ -690,6 +750,13 @@ Future<void> uploadFileFastOrEnqueue(
UploadTask task, UploadTask task,
MediaFileService media, MediaFileService media,
) async { ) async {
if (await _checkAndRecoverMissingUploadRequest(
media,
triggerBackgroundUpload: true,
)) {
return;
}
final requestMultipart = http.MultipartRequest( final requestMultipart = http.MultipartRequest(
'POST', 'POST',
Uri.parse(task.url), Uri.parse(task.url),

View file

@ -19,6 +19,7 @@ import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/user.service.dart' show UserService;
import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -100,14 +101,18 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
if (receiptId == null && receipt == null) return null; if (receiptId == null && receipt == null) return null;
try { try {
if (receipt == null) { final targetReceiptId = receipt?.receiptId ?? receiptId!;
// ignore: parameter_assignments final loadedReceipt = await twonlyDB.receiptsDao.getReceiptById(
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); targetReceiptId,
if (receipt == null) { );
Log.warn('[$receiptId] Receipt not found.'); if (loadedReceipt == null) {
return null; Log.info(
} '[$targetReceiptId] Receipt not found (might have been processed or deleted).',
);
return null;
} }
// ignore: parameter_assignments
receipt = loadedReceipt;
if (receipt.retryCount >= 2) { if (receipt.retryCount >= 2) {
// After two retries, change the receiptId. This addresses a bug where the receiver received the message and marked it as received, // After two retries, change the receiptId. This addresses a bug where the receiver received the message and marked it as received,
@ -137,7 +142,7 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
if (!onlyReturnEncryptedData && if (!onlyReturnEncryptedData &&
receipt.ackByServerAt != null && receipt.ackByServerAt != null &&
receipt.markForRetry == null) { receipt.markForRetry == null) {
Log.error('Message already uploaded and mark for retry is not set!'); Log.info('Message already uploaded and mark for retry is not set.');
return null; return null;
} }
@ -176,7 +181,17 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
Uint8List.fromList(message.encryptedContent), Uint8List.fromList(message.encryptedContent),
); );
if (cipherText == null) { if (cipherText == null) {
Log.error('Could not encrypt the message. Aborting and trying again.'); Log.error(
'[${receipt.receiptId}] Could not encrypt the message for user ${receipt.contactId}. Aborting and trying again.',
);
if (receipt.messageId != null) {
await twonlyDB.messagesDao.handleMessageAckByServer(
receipt.contactId,
receipt.messageId!,
clock.now(),
);
}
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
return null; return null;
} }
message.encryptedContent = cipherText.serialize(); message.encryptedContent = cipherText.serialize();
@ -434,7 +449,7 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact( final openReceipts = await twonlyDB.receiptsDao.getReceiptCountForContact(
contactId, contactId,
); );
if (openReceipts > 6) { if (openReceipts > 10) {
// this prevents that these types of messages are send in case the receiver is offline // this prevents that these types of messages are send in case the receiver is offline
return null; return null;
} }
@ -443,12 +458,26 @@ Future<(Uint8List, Uint8List?)?> sendCipherText(
userService.currentUser.avatarCounter, userService.currentUser.avatarCounter,
); );
if (userService.currentUser.isUserDiscoveryEnabled && messageId != null) { {
final contact = await twonlyDB.contactsDao.getContactById(contactId); if (userService.currentUser.askForFriendPromotions) {
if (UserDiscoveryService.isContactAllowed(contact)) { final contacts = await twonlyDB.contactsDao.getAllContacts();
final version = await UserDiscoveryService.getCurrentVersion(); final contactCount = contacts.where((c) => c.accepted).length;
if (version != null) { if (contactCount > 5) {
encryptedContent.senderUserDiscoveryVersion = version; await UserService.update((u) {
u.askForFriendPromotions = false;
});
} else {
encryptedContent.askForFriendPromotions = true;
}
}
if (userService.currentUser.isUserDiscoveryEnabled && messageId != null) {
final contact = await twonlyDB.contactsDao.getContactById(contactId);
if (UserDiscoveryService.isContactAllowed(contact)) {
final version = await UserDiscoveryService.getCurrentVersion();
if (version != null) {
encryptedContent.senderUserDiscoveryVersion = version;
}
} }
} }
} }

View file

@ -5,6 +5,7 @@ import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
@ -32,6 +33,7 @@ import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -54,9 +56,13 @@ Future<void> handleServerMessage(server.ServerToClient msg) async {
Log.info( Log.info(
'Got ${msg.v0.newMessages.newMessages.length} messages from the server.', 'Got ${msg.v0.newMessages.newMessages.length} messages from the server.',
); );
final brokenSessionsInCurrentBatch = <int>{};
for (final newMessage in msg.v0.newMessages.newMessages) { for (final newMessage in msg.v0.newMessages.newMessages) {
try { try {
await handleClient2ClientMessage(newMessage); await handleClient2ClientMessage(
newMessage,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
);
} catch (e) { } catch (e) {
Log.error(e); Log.error(e);
} }
@ -81,7 +87,10 @@ DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
final Map<String, Mutex> _messageLocks = {}; final Map<String, Mutex> _messageLocks = {};
Future<void> handleClient2ClientMessage(NewMessage newMessage) async { Future<void> handleClient2ClientMessage(
NewMessage newMessage, {
Set<int>? brokenSessionsInCurrentBatch,
}) async {
final body = Uint8List.fromList(newMessage.body); final body = Uint8List.fromList(newMessage.body);
final message = Message.fromBuffer(body); final message = Message.fromBuffer(body);
final receiptId = message.receiptId; final receiptId = message.receiptId;
@ -95,7 +104,11 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
} }
await mutex.protect(() async { await mutex.protect(() async {
try { try {
await _handleClient2ClientMessage(newMessage, message); await _handleClient2ClientMessage(
newMessage,
message,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
);
} finally { } finally {
_messageLocks.remove(receiptId); _messageLocks.remove(receiptId);
} }
@ -104,11 +117,27 @@ Future<void> handleClient2ClientMessage(NewMessage newMessage) async {
Future<void> _handleClient2ClientMessage( Future<void> _handleClient2ClientMessage(
NewMessage newMessage, NewMessage newMessage,
Message message, Message message, {
) async { Set<int>? brokenSessionsInCurrentBatch,
}) async {
final fromUserId = newMessage.fromUserId.toInt(); final fromUserId = newMessage.fromUserId.toInt();
final receiptId = message.receiptId; final receiptId = message.receiptId;
if (brokenSessionsInCurrentBatch?.contains(fromUserId) == true) {
// This happens when a session goes out of sync (e.g. wrong message order).
// We skip the remaining messages in the batch because each failed decryption
// attempt is extremely slow (~1.2s) and would otherwise freeze the app.
// By returning early, we skip gotReceipt() and error responses.
// The server still deletes the batch since we ACK the entire batch later.
// The sender keeps the message unacknowledged. Once they process our SESSION_OUT_OF_SYNC
// error and establish a new session, their retry logic will automatically re-encrypt
// and re-send these messages with the new keys.
Log.info(
'Skipping message from $fromUserId - session known broken in this batch',
);
return;
}
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) { if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
return; return;
} }
@ -198,6 +227,7 @@ Future<void> _handleClient2ClientMessage(
encryptedContentRaw, encryptedContentRaw,
message.type, message.type,
receiptId, receiptId,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
); );
if (plainTextContent != null) { if (plainTextContent != null) {
response = Message( response = Message(
@ -210,6 +240,8 @@ Future<void> _handleClient2ClientMessage(
type: Message_Type.CIPHERTEXT, type: Message_Type.CIPHERTEXT,
encryptedContent: encryptedContent.writeToBuffer(), encryptedContent: encryptedContent.writeToBuffer(),
); );
// Use Value.absent() for CIPHERTEXT messages so that insertReceipt generates a new UUID.
// This prevents receipt ID collisions and ensures the recipient's ACK is tracked correctly.
receiptIdDB = const Value.absent(); receiptIdDB = const Value.absent();
} else { } else {
// Message was successful processed // Message was successful processed
@ -218,8 +250,9 @@ Future<void> _handleClient2ClientMessage(
response ??= Message(type: Message_Type.SENDER_DELIVERY_RECEIPT); response ??= Message(type: Message_Type.SENDER_DELIVERY_RECEIPT);
String? targetReceiptId;
try { try {
await twonlyDB.receiptsDao.insertReceipt( final inserted = await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion( ReceiptsCompanion(
receiptId: receiptIdDB ?? Value(receiptId), receiptId: receiptIdDB ?? Value(receiptId),
contactId: Value(fromUserId), contactId: Value(fromUserId),
@ -227,10 +260,18 @@ Future<void> _handleClient2ClientMessage(
contactWillSendsReceipt: const Value(false), contactWillSendsReceipt: const Value(false),
), ),
); );
// Use the inserted receipt's ID because for CIPHERTEXT messages we generate a new UUID
// (receiptIdDB is Value.absent()) to avoid ID collisions and properly track individual ACKs.
targetReceiptId = inserted?.receiptId;
} catch (e) { } catch (e) {
Log.warn('[$receiptId] Error inserting receipt: $e'); Log.warn('[$receiptId] Error inserting receipt: $e');
} }
await tryToSendCompleteMessage(receiptId: receiptId, blocking: false); if (targetReceiptId != null) {
await tryToSendCompleteMessage(
receiptId: targetReceiptId,
blocking: false,
);
}
} }
case Message_Type.TEST_NOTIFICATION: case Message_Type.TEST_NOTIFICATION:
break; break;
@ -240,7 +281,11 @@ Future<void> _handleClient2ClientMessage(
await twonlyDB.receiptsDao.gotReceipt(receiptId); await twonlyDB.receiptsDao.gotReceipt(receiptId);
Log.info('[$receiptId] Finished processing'); Log.info('[$receiptId] Finished processing');
} catch (e) { } catch (e) {
Log.error('[$receiptId] Error marking message as received: $e'); Log.warn('[$receiptId] Error marking message as received: $e');
Log.error(
'Error marking message as received: $e',
onlyIfSentryEnabled: true,
);
} }
} }
@ -248,13 +293,15 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
int fromUserId, int fromUserId,
Uint8List encryptedContentRaw, Uint8List encryptedContentRaw,
Message_Type messageType, Message_Type messageType,
String receiptId, String receiptId, {
) async { Set<int>? brokenSessionsInCurrentBatch,
}) async {
Log.info('[$receiptId] calling signalDecryptMessage'); Log.info('[$receiptId] calling signalDecryptMessage');
var (encryptedContent, decryptionErrorType) = await signalDecryptMessage( var (encryptedContent, decryptionErrorType) = await signalDecryptMessage(
fromUserId, fromUserId,
encryptedContentRaw, encryptedContentRaw,
messageType.value, messageType.value,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
); );
if (encryptedContent == null) { if (encryptedContent == null) {
@ -271,7 +318,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
Log.info('[$receiptId] Calling handleEncryptedMessage'); Log.info('[$receiptId] Calling handleEncryptedMessage');
final (a, b) = await handleEncryptedMessage( final result = await handleEncryptedMessage(
fromUserId, fromUserId,
encryptedContent, encryptedContent,
messageType, messageType,
@ -280,9 +327,9 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
Log.info('[$receiptId] Finished handleEncryptedMessage'); Log.info('[$receiptId] Finished handleEncryptedMessage');
if (a == null && b == null) { if (result.responseCipherText == null && result.responsePlaintext == null) {
unawaited(updateLastServerMessageTimestamp()); unawaited(FcmNotificationService.updateLastServerMessageTimestamp());
if (Platform.isAndroid) { if (Platform.isAndroid && result.showPushNotification) {
// Message was handled without any error. Show push notification to the user for Android. // Message was handled without any error. Show push notification to the user for Android.
await showPushNotificationFromServerMessages( await showPushNotificationFromServerMessages(
fromUserId, fromUserId,
@ -291,10 +338,10 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
} }
} }
return (a, b); return (result.responseCipherText, result.responsePlaintext);
} }
Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage( Future<DecryptedMessageResult> handleEncryptedMessage(
int fromUserId, int fromUserId,
EncryptedContent content, EncryptedContent content,
Message_Type messageType, Message_Type messageType,
@ -319,19 +366,28 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
} }
if (content.hasAskForFriendPromotions() && content.askForFriendPromotions) {
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
if (contact != null && contact.askForFriendPromotions == null) {
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(askForFriendPromotions: Value(true)),
);
}
}
if (content.hasContactRequest()) { if (content.hasContactRequest()) {
if (!await handleContactRequest( if (!await handleContactRequest(
fromUserId, fromUserId,
content.contactRequest, content.contactRequest,
receiptId, receiptId,
)) { )) {
return ( return DecryptedMessageResult(
null, responsePlaintext: PlaintextContent()
PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage(), ..retryControlError = PlaintextContent_RetryErrorMessage(),
); );
} }
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasErrorMessages()) { if (content.hasErrorMessages()) {
@ -339,8 +395,27 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.errorMessages, content.errorMessages,
receiptId, receiptId,
groupId: content.hasGroupId() ? content.groupId : null,
); );
return (null, null); return const DecryptedMessageResult(showPushNotification: false);
}
if (content.hasPasswordlessRecovery()) {
await PasswordlessRecoveryService.handlePasswordlessRecovery(
fromUserId,
content.passwordlessRecovery,
receiptId,
);
return const DecryptedMessageResult();
}
if (content.hasPasswordlessRecoveryHeartbeat()) {
await PasswordlessRecoveryService.handlePasswordlessRecoveryHeartbeat(
fromUserId,
content.passwordlessRecoveryHeartbeat,
receiptId,
);
return const DecryptedMessageResult();
} }
if (content.hasContactUpdate()) { if (content.hasContactUpdate()) {
@ -350,7 +425,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
senderProfileCounter, senderProfileCounter,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult(showPushNotification: false);
} }
if (content.hasUserDiscoveryRequest()) { if (content.hasUserDiscoveryRequest()) {
@ -359,7 +434,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.userDiscoveryRequest, content.userDiscoveryRequest,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult(showPushNotification: false);
} }
if (content.hasUserDiscoveryUpdate()) { if (content.hasUserDiscoveryUpdate()) {
@ -368,12 +443,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.userDiscoveryUpdate, content.userDiscoveryUpdate,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasPushKeys()) { if (content.hasPushKeys()) {
await handlePushKey(fromUserId, content.pushKeys, receiptId); await handlePushKey(fromUserId, content.pushKeys, receiptId);
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasMessageUpdate()) { if (content.hasMessageUpdate()) {
@ -382,7 +457,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.messageUpdate, content.messageUpdate,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasKeyVerificationProof()) { if (content.hasKeyVerificationProof()) {
@ -390,7 +465,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
fromUserId, fromUserId,
content.keyVerificationProof.calculatedMac, content.keyVerificationProof.calculatedMac,
); );
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasMediaUpdate()) { if (content.hasMediaUpdate()) {
@ -399,12 +474,19 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.mediaUpdate, content.mediaUpdate,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult();
} }
if (!content.hasGroupId()) { if (!content.hasGroupId()) {
Log.error('[$receiptId] Messages should have a groupId $fromUserId.'); final type = _getEncryptedContentType(content);
return (null, null); Log.warn(
'[$receiptId] Messages should have a groupId $fromUserId. Type: $type',
);
Log.error(
'Messages should have a groupId. Type: $type',
onlyIfSentryEnabled: true,
);
return const DecryptedMessageResult();
} }
if (content.hasGroupCreate()) { if (content.hasGroupCreate()) {
@ -414,11 +496,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.groupCreate, content.groupCreate,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult();
} }
/// Verify that the user is (still) in that group... /// Verify that the user is (still) in that group...
if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) {
// Check if this is a direct chat...
if (getUUIDforDirectChat(userService.currentUser.userId, fromUserId) == if (getUUIDforDirectChat(userService.currentUser.userId, fromUserId) ==
content.groupId) { content.groupId) {
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
@ -429,18 +512,17 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
if (contact == null || !contact.accepted || contact.deletedByUser) { if (contact == null || !contact.accepted || contact.deletedByUser) {
await handleNewContactRequest(fromUserId); await handleNewContactRequest(fromUserId);
Log.error( Log.warn(
'[$receiptId] User tries to send message to direct chat while the user does not exist!', '[$receiptId] User tries to send message to direct chat while the user does not exist!',
); );
return ( return DecryptedMessageResult(
EncryptedContent( responseCipherText: EncryptedContent(
errorMessages: EncryptedContent_ErrorMessages( errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type type: EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD, .ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
relatedReceiptId: receiptId, relatedReceiptId: receiptId,
), ),
), ),
null,
); );
} }
Log.info( Log.info(
@ -454,27 +536,35 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
} else { } else {
if (content.hasGroupJoin()) { if (content.hasGroupJoin()) {
Log.error( Log.warn(
'[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.', '[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.',
); );
// In case the group join was received before the GroupCreate the sender should send it later again. // In case the group join was received before the GroupCreate the sender should send it later again.
return ( return DecryptedMessageResult(
null, responsePlaintext: PlaintextContent()
PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage(), ..retryControlError = PlaintextContent_RetryErrorMessage(),
); );
} }
Log.error( Log.warn(
'[$receiptId] User $fromUserId tried to access group ${content.groupId}.', '[$receiptId] User $fromUserId tried to access group ${content.groupId}. Sending GROUP_NOT_FOUND_OR_NOT_A_MEMBER error.',
);
return DecryptedMessageResult(
responseCipherText: EncryptedContent(
groupId: content.groupId,
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type
.GROUP_NOT_FOUND_OR_NOT_A_MEMBER,
relatedReceiptId: receiptId,
),
),
); );
return (null, null);
} }
} }
if (content.hasFlameSync()) { if (content.hasFlameSync()) {
await handleFlameSync(content.groupId, content.flameSync, receiptId); await handleFlameSync(content.groupId, content.flameSync, receiptId);
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasGroupUpdate()) { if (content.hasGroupUpdate()) {
@ -484,7 +574,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.groupUpdate, content.groupUpdate,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasGroupJoin()) { if (content.hasGroupJoin()) {
@ -494,13 +584,12 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.groupJoin, content.groupJoin,
receiptId, receiptId,
)) { )) {
return ( return DecryptedMessageResult(
null, responsePlaintext: PlaintextContent()
PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage(), ..retryControlError = PlaintextContent_RetryErrorMessage(),
); );
} }
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasResendGroupPublicKey()) { if (content.hasResendGroupPublicKey()) {
@ -510,7 +599,7 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.groupJoin, content.groupJoin,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasAdditionalDataMessage()) { if (content.hasAdditionalDataMessage()) {
@ -520,17 +609,17 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.additionalDataMessage, content.additionalDataMessage,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasTextMessage()) { if (content.hasTextMessage()) {
await handleTextMessage( final isNewText = await handleTextMessage(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.textMessage, content.textMessage,
receiptId, receiptId,
); );
return (null, null); return DecryptedMessageResult(showPushNotification: isNewText);
} }
if (content.hasReaction()) { if (content.hasReaction()) {
@ -540,17 +629,17 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
content.reaction, content.reaction,
receiptId, receiptId,
); );
return (null, null); return const DecryptedMessageResult();
} }
if (content.hasMedia()) { if (content.hasMedia()) {
await handleMedia( final isNewMedia = await handleMedia(
fromUserId, fromUserId,
content.groupId, content.groupId,
content.media, content.media,
receiptId, receiptId,
); );
return (null, null); return DecryptedMessageResult(showPushNotification: isNewMedia);
} }
if (content.hasTypingIndicator()) { if (content.hasTypingIndicator()) {
@ -562,5 +651,39 @@ Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessage(
); );
} }
return (null, null); return const DecryptedMessageResult();
}
String _getEncryptedContentType(EncryptedContent content) {
if (content.hasMessageUpdate()) return 'messageUpdate';
if (content.hasMedia()) return 'media';
if (content.hasMediaUpdate()) return 'mediaUpdate';
if (content.hasContactUpdate()) return 'contactUpdate';
if (content.hasContactRequest()) return 'contactRequest';
if (content.hasFlameSync()) return 'flameSync';
if (content.hasPushKeys()) return 'pushKeys';
if (content.hasReaction()) return 'reaction';
if (content.hasTextMessage()) return 'textMessage';
if (content.hasGroupCreate()) return 'groupCreate';
if (content.hasGroupJoin()) return 'groupJoin';
if (content.hasGroupUpdate()) return 'groupUpdate';
if (content.hasResendGroupPublicKey()) return 'resendGroupPublicKey';
if (content.hasErrorMessages()) return 'errorMessages';
if (content.hasAdditionalDataMessage()) return 'additionalDataMessage';
if (content.hasTypingIndicator()) return 'typingIndicator';
if (content.hasUserDiscoveryRequest()) return 'userDiscoveryRequest';
if (content.hasUserDiscoveryUpdate()) return 'userDiscoveryUpdate';
if (content.hasKeyVerificationProof()) return 'keyVerificationProof';
return 'unknown';
}
class DecryptedMessageResult {
const DecryptedMessageResult({
this.responseCipherText,
this.responsePlaintext,
this.showPushNotification = true,
});
final EncryptedContent? responseCipherText;
final PlaintextContent? responsePlaintext;
final bool showPushNotification;
} }

View file

@ -105,8 +105,9 @@ class BackupService {
))) { ))) {
final backupId = await RustBackupIdentity.getBackupId(); final backupId = await RustBackupIdentity.getBackupId();
if (backupId == null) { if (backupId == null) {
Log.error('No backup password was set by the user.'); Log.warn('No backup password was set by the user.');
backup.identityState = LastBackupUploadState.failed; backup.identityState = LastBackupUploadState.failed;
await UserService.update((u) => u.isBackupEnabled = false);
} else { } else {
Log.info('Performing a identity backup.'); Log.info('Performing a identity backup.');
final encryptedBackup = final encryptedBackup =
@ -162,7 +163,7 @@ class BackupService {
(backupDownloadToken, backupArchive) = (backupDownloadToken, backupArchive) =
await RustBackupArchive.createBackupArchive(); await RustBackupArchive.createBackupArchive();
} catch (e) { } catch (e) {
Log.error(e); Log.warn('Creating archive backup failed: $e');
return; return;
} }
Log.info( Log.info(
@ -324,6 +325,26 @@ class BackupService {
return _nextBackupStage(); return _nextBackupStage();
} }
static Future<RecoveryError?> startPasswordlessBackupRecovery(
int userId,
String username,
Uint8List keyManagerBytes,
) async {
final state = BackupRecovery(
username: username,
password: '',
userId: userId,
)..state = BackupRecoveryState.archiveBackupStarted;
await deleteLocalUserData();
// Import KeyManager keys into secure storage & in-memory key manager
await RustKeyManager.importSerialized(serializedBytes: keyManagerBytes);
await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson());
return _nextBackupStage();
}
static Future<(Uint8List?, RecoveryError?)> _downloadBackup( static Future<(Uint8List?, RecoveryError?)> _downloadBackup(
String backupServerUrl, String backupServerUrl,
) async { ) async {
@ -337,7 +358,7 @@ class BackupService {
}, },
); );
} catch (e) { } catch (e) {
Log.error('Error fetching backup: $e'); Log.warn('Error fetching backup: $e');
return (null, RecoveryError.noInternet); return (null, RecoveryError.noInternet);
} }

View file

@ -139,6 +139,7 @@ Future<bool> createNewGroup(String groupName, List<Contact> members) async {
groupCreate: EncryptedContent_GroupCreate( groupCreate: EncryptedContent_GroupCreate(
stateKey: stateEncryptionKey, stateKey: stateEncryptionKey,
groupPublicKey: myGroupKey.getPublicKey().serialize(), groupPublicKey: myGroupKey.getPublicKey().serialize(),
groupName: group.groupName,
), ),
), ),
); );
@ -770,6 +771,7 @@ Future<bool> addNewGroupMembers(
groupCreate: EncryptedContent_GroupCreate( groupCreate: EncryptedContent_GroupCreate(
stateKey: group.stateEncryptionKey, stateKey: group.stateEncryptionKey,
groupPublicKey: keyPair.getPublicKey().serialize(), groupPublicKey: keyPair.getPublicKey().serialize(),
groupName: group.groupName,
), ),
), ),
); );

View file

@ -1,9 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
@ -13,6 +13,8 @@ import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart'
show PasswordlessRecoveryService;
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -30,6 +32,12 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
// Check if this is the QR code link which was // Check if this is the QR code link which was
// therefore scanned with the system camera // therefore scanned with the system camera
if (kDebugMode &&
uri.toString().startsWith(PasswordlessRecoveryService.linkPrefix)) {
await PasswordlessRecoveryService.handleRecoveryLink(uri.toString());
return true;
}
if (uri.toString().startsWith(QrCodeUtils.linkPrefix)) { if (uri.toString().startsWith(QrCodeUtils.linkPrefix)) {
final result = await QrCodeUtils.handleQrCodeLink(uri.toString()); final result = await QrCodeUtils.handleQrCodeLink(uri.toString());

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -13,7 +14,7 @@ import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
class KeyVerificationService { class KeyVerificationService {
static Future<List<int>> getNewSecretVerificationToken() async { static Future<List<int>> getNewSecretVerificationToken() async {
@ -77,12 +78,14 @@ class KeyVerificationService {
final contact = await twonlyDB.contactsDao.getContactById(fromUserId); final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
final context = rootNavigatorKey.currentContext; final context = rootNavigatorKey.currentContext;
if (context != null && context.mounted && contact != null) { if (context != null && context.mounted && contact != null) {
showSnackbar( unawaited(
context, VerificationSuccessDialog.show(
context.lang.secretQrTokenVerifiedSnackbar( context,
getContactDisplayName(contact), contact,
message: context.lang.secretQrTokenVerifiedSnackbar(
getContactDisplayName(contact),
),
), ),
level: SnackbarLevel.success,
); );
} }
return; return;
@ -91,6 +94,29 @@ class KeyVerificationService {
Log.error('No valid secret token could be found...'); Log.error('No valid secret token could be found...');
} }
static Future<void> verifySharedContact({
required int contactId,
required List<int> sharedPublicIdentityKey,
required int senderId,
}) async {
final publicIdentityKey = await getPublicKeyFromContact(contactId);
if (publicIdentityKey == null) {
Log.info('No public key stored for contact $contactId');
return;
}
if (publicIdentityKey.equals(sharedPublicIdentityKey)) {
Log.info('Verified a user which was shared by a contact');
await twonlyDB.keyVerificationDao.addKeyVerification(
contactId,
VerificationType.contactSharedByVerified,
verifiedBy: senderId,
);
} else {
Log.error('Public identity keys do not match for contact $contactId');
}
}
} }
Future<List<int>> _createVerificationBytes( Future<List<int>> _createVerificationBytes(

View file

@ -33,17 +33,18 @@ Future<void> compressImage(
Log.info('Compressed images size in bytes: ${compressedBytes.length}'); Log.info('Compressed images size in bytes: ${compressedBytes.length}');
if (compressedBytes.length >= 1 * 1000 * 1000) { if (compressedBytes.length >= 2 * 1000 * 1000) {
// if the media file is over 1MB compress it with 60% // if the media file is over 1MB compress it with 60%
final tmpCompressedBytes = await FlutterImageCompress.compressWithFile( final tmpCompressedBytes = await FlutterImageCompress.compressWithFile(
sourceFile.path, sourceFile.path,
format: CompressFormat.webp, format: CompressFormat.webp,
quality: 60, quality: 60,
); );
if (tmpCompressedBytes != null) { if (tmpCompressedBytes == null) {
Log.error( Log.error(
'Could not compress media file with 60%: $sourceFile. Sending original 90% compressed file.', 'Could not compress media file with 60%: $sourceFile. Sending original 90% compressed file.',
); );
} else {
compressedBytes = tmpCompressedBytes; compressedBytes = tmpCompressedBytes;
} }
} }
@ -106,11 +107,11 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
}, },
); );
} catch (e) { } catch (e) {
Log.error('during video compression: $e'); Log.warn('during video compression: $e');
} }
if (compressedPath == null) { if (compressedPath == null) {
Log.error('Could not compress video using original video.'); Log.warn('Could not compress video using original video.');
// as a fall back use the non compressed version // as a fall back use the non compressed version
media.ffmpegOutputPath.copySync(media.tempPath.path); media.ffmpegOutputPath.copySync(media.tempPath.path);
} }

View file

@ -252,7 +252,7 @@ class MediaFileService {
Future<void> compressMedia() async { Future<void> compressMedia() async {
if (!originalPath.existsSync()) { if (!originalPath.existsSync()) {
Log.error('Could not compress as original media does not exists.'); Log.warn('Could not compress as original media does not exists.');
return; return;
} }
@ -296,7 +296,8 @@ class MediaFileService {
(thumbnailPath.existsSync() && thumbnailPath.lengthSync() > 0) || (thumbnailPath.existsSync() && thumbnailPath.lengthSync() > 0) ||
mediaFile.type == MediaType.audio || mediaFile.type == MediaType.audio ||
((mediaFile.type == MediaType.image || mediaFile.type == MediaType.gif) && ((mediaFile.type == MediaType.image || mediaFile.type == MediaType.gif) &&
storedPath.existsSync() && storedPath.lengthSync() > 0); storedPath.existsSync() &&
storedPath.lengthSync() > 0);
Future<void> storeMediaFile() async { Future<void> storeMediaFile() async {
Log.info('Storing media file ${mediaFile.mediaId}'); Log.info('Storing media file ${mediaFile.mediaId}');
@ -314,17 +315,21 @@ class MediaFileService {
await tempPath.copy(storedPath.path); await tempPath.copy(storedPath.path);
if (userService.currentUser.storeMediaFilesInGallery) { if (userService.currentUser.storeMediaFilesInGallery) {
if (mediaFile.type == MediaType.video) { if (mediaFile.type == MediaType.video) {
await saveVideoToGallery(storedPath.path); await saveVideoToGallery(
storedPath.path,
name: mediaFile.mediaId,
);
} else { } else {
await saveImageToGallery( await saveImageToGallery(
storedPath.readAsBytesSync(), storedPath.readAsBytesSync(),
createdAt: mediaFile.createdAt, createdAt: mediaFile.createdAt,
name: mediaFile.mediaId,
); );
} }
} }
} else { } else {
Log.error( Log.warn(
'Could not store image neither as ${tempPath.path} does not exists.', 'Could not store image locally as ${tempPath.path} does not exist.',
); );
} }
unawaited(createThumbnail()); unawaited(createThumbnail());
@ -465,7 +470,7 @@ class MediaFileService {
format: CompressFormat.webp, format: CompressFormat.webp,
quality: 90, quality: 90,
); );
if (webpBytes.isNotEmpty) { if (webpBytes.isNotEmpty) {
await storedPath.writeAsBytes(webpBytes); await storedPath.writeAsBytes(webpBytes);
} else { } else {
@ -499,7 +504,7 @@ class MediaFileService {
); );
await updateFromDB(); await updateFromDB();
} catch (e) { } catch (e) {
Log.error( Log.warn(
'Error auto-cropping transparent borders for mediaId ${mediaFile.mediaId}: $e', 'Error auto-cropping transparent borders for mediaId ${mediaFile.mediaId}: $e',
); );
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(

View file

@ -112,7 +112,7 @@ Future<bool> createThumbnailsForImage(
); );
return true; return true;
} else { } else {
Log.error('Compressed image thumbnail is empty or missing.'); Log.warn('Compressed image thumbnail is empty or missing.');
try { try {
if (destinationFile.existsSync()) { if (destinationFile.existsSync()) {
destinationFile.deleteSync(); destinationFile.deleteSync();

View file

@ -158,9 +158,21 @@ Future<void> runMigrations() async {
} }
await UserService.update((u) => u.appVersion = 116); await UserService.update((u) => u.appVersion = 116);
} }
if (userService.currentUser.appVersion < 117) {
final contacts = await twonlyDB.contactsDao.getAllContacts();
final contactCount = contacts.where((c) => c.accepted).length;
await UserService.update((u) {
u.appVersion = 117;
if (contactCount > 5) {
u.askForFriendPromotions = false;
}
});
}
if (kDebugMode) { if (kDebugMode) {
assert( assert(
AppState.latestAppVersionId == 116, AppState.latestAppVersionId == 117,
'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.', 'Forgot to update the target version in runMigrations() after incrementing AppState.latestAppVersionId.',
); );
assert( assert(

View file

@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
@ -13,6 +13,7 @@ import 'package:twonly/src/localization/generated/app_localizations_de.dart';
import 'package:twonly/src/localization/generated/app_localizations_en.dart'; import 'package:twonly/src/localization/generated/app_localizations_en.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -70,69 +71,6 @@ Future<void> showPushNotificationFromServerMessages(
} }
} }
Future<void> handlePushData(String pushDataB64) async {
try {
final pushData = EncryptedPushNotification.fromBuffer(
base64.decode(pushDataB64),
);
PushNotification? pushNotification;
PushUser? foundPushUser;
if (pushData.keyId == 0) {
final key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
pushNotification = await tryDecryptMessage(key, pushData);
} else {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
for (final pushUser in pushUsers) {
for (final key in pushUser.pushKeys) {
if (key.id == pushData.keyId) {
pushNotification = await tryDecryptMessage(key.key, pushData);
if (pushNotification != null) {
foundPushUser = pushUser;
break;
}
}
}
// found correct key and user
if (foundPushUser != null) break;
}
}
if (pushNotification != null) {
if (pushNotification.kind == PushKind.TEST_NOTIFICATION) {
await customLocalPushNotification(
'Test notification',
'This is a test notification.',
);
} else if (foundPushUser != null) {
if (pushNotification.hasMessageId()) {
if (isUUIDNewer(
foundPushUser.lastMessageId,
pushNotification.messageId,
)) {
Log.info(
'Got a push notification for a message which was already opened.',
);
return;
}
}
await showLocalPushNotification(foundPushUser, pushNotification);
} else {
await showLocalPushNotificationWithoutUserId(pushNotification);
}
}
} catch (e) {
Log.error(e);
final lang = getLocalizations();
await customLocalPushNotification(
lang.notificationTitleUnknown,
lang.notificationBodyUnknown,
);
}
}
Future<PushNotification?> tryDecryptMessage( Future<PushNotification?> tryDecryptMessage(
List<int> key, List<int> key,
EncryptedPushNotification push, EncryptedPushNotification push,
@ -172,6 +110,32 @@ Future<void> showLocalPushNotification(
return; return;
} }
var targetGroupId = groupId;
if (targetGroupId == null) {
try {
if (userService.isUserCreated) {
targetGroupId = getUUIDforDirectChat(
userService.currentUser.userId,
pushUser.userId.toInt(),
);
}
} catch (_) {}
}
if (targetGroupId != null) {
try {
final currentUri = routerProvider.routerDelegate.currentConfiguration.uri;
if (currentUri.path.contains(targetGroupId)) {
Log.info(
'Suppressing local push notification because chat with group $targetGroupId is currently open.',
);
return;
}
} catch (e) {
Log.error('Error checking current route: $e');
}
}
title = pushUser.displayName; title = pushUser.displayName;
body = getPushNotificationText(pushNotification); body = getPushNotificationText(pushNotification);
if (body == '') { if (body == '') {
@ -195,6 +159,7 @@ Future<void> showLocalPushNotification(
ticker: 'You got a new message.', ticker: 'You got a new message.',
largeIcon: styleInformation, largeIcon: styleInformation,
icon: 'ic_launcher_foreground', icon: 'ic_launcher_foreground',
groupKey: 'com.twonly.messages',
); );
const darwinNotificationDetails = DarwinNotificationDetails(); const darwinNotificationDetails = DarwinNotificationDetails();
@ -226,43 +191,29 @@ Future<void> showLocalPushNotification(
notificationDetails, notificationDetails,
payload: payload, payload: payload,
); );
}
Future<void> showLocalPushNotificationWithoutUserId( if (Platform.isAndroid) {
PushNotification pushNotification, final summaryAndroidDetails = AndroidNotificationDetails(
) async { '0',
final lang = getLocalizations(); lang.notificationCategoryMessageTitle,
channelDescription: lang.notificationCategoryMessageDesc,
var title = lang.notificationTitleUnknown; importance: Importance.max,
var body = lang.notificationBodyUnknown; priority: Priority.max,
groupKey: 'com.twonly.messages',
if (pushNotification.kind == PushKind.CONTACT_REQUEST) { setAsGroupSummary: true,
title = lang.you; icon: 'ic_launcher_foreground',
body = lang.notificationContactRequestUnknownUser; );
final summaryNotificationDetails = NotificationDetails(
android: summaryAndroidDetails,
);
await flutterLocalNotificationsPlugin.show(
0,
lang.notificationCategoryMessageTitle,
'',
summaryNotificationDetails,
payload: Routes.chats,
);
} }
final androidNotificationDetails = AndroidNotificationDetails(
'0',
lang.notificationCategoryMessageTitle,
channelDescription: lang.notificationCategoryMessageDesc,
importance: Importance.max,
priority: Priority.max,
ticker: 'You got a new message.',
);
const darwinNotificationDetails = DarwinNotificationDetails();
final notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: darwinNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
2,
title,
body,
notificationDetails,
payload: pushNotification.kind.name,
);
} }
Future<String?> getAvatarIcon(int contactId) async { Future<String?> getAvatarIcon(int contactId) async {

View file

@ -0,0 +1,29 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/utils/log.dart';
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final isInitialized = await initBackgroundExecution();
await setupPushNotification();
Log.info('Handling a background message: ${message.messageId}');
await FcmNotificationService.handleRemoteMessage(message);
if (Platform.isAndroid) {
if (isInitialized) {
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
}
} else {
// make sure every thing run...
await Future.delayed(const Duration(milliseconds: 2000));
}
}

View file

@ -1,5 +1,3 @@
// ignore_for_file: unreachable_from_main
import 'dart:async'; import 'dart:async';
import 'dart:io' show Platform; import 'dart:io' show Platform;
@ -7,13 +5,11 @@ import 'package:firebase_app_installations/firebase_app_installations.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/services/background/callback_dispatcher.background.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.background.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -21,240 +17,253 @@ import '../../../firebase_options.dart';
// see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de // see more here: https://firebase.google.com/docs/cloud-messaging/flutter/receive?hl=de
Future<void> checkForTokenUpdates() async { class FcmNotificationService {
try { static Future<void> initStartup() async {
if (!userService.isUserCreated) return; await Firebase.initializeApp(
if (Platform.isIOS) { options: DefaultFirebaseOptions.currentPlatform,
var apnsToken = await FirebaseMessaging.instance.getAPNSToken(); );
for (var i = 0; i < 20; i++) {
if (apnsToken != null) break; FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
await Future<void>.delayed(const Duration(seconds: 1));
apnsToken = await FirebaseMessaging.instance.getAPNSToken(); FirebaseMessaging.onMessage.listen(handleRemoteMessage);
} }
if (apnsToken == null) {
Log.error('Could not get APNS token even after 20s...'); static Future<void> initAfterUserLoaded() async {
unawaited(_checkForTokenUpdates());
unawaited(_checkFcmHealthAndResetIfNeeded());
}
static Future<void> initFCMAfterAuthenticated({bool force = false}) async {
final fcmToken = userService.currentUser.fcmToken;
if (userService.currentUser.updateFCMToken || force) {
if (fcmToken == null) {
Log.error('FCM token could not be updated as it is empty');
await _checkForTokenUpdates();
return; return;
} }
} final res = await apiService.updateFCMToken(
fcmToken,
final fcmToken = await FirebaseMessaging.instance.getToken(); );
if (fcmToken == null) { if (res.isSuccess) {
Log.error('Could not get fcm token'); Log.info('Uploaded new FCM token!');
return; await UserService.update((u) {
} u.updateFCMToken = false;
Log.info('Loaded FCM token.');
if (userService.currentUser.fcmToken == null ||
fcmToken != userService.currentUser.fcmToken) {
Log.info('Got new FCM token.');
await UserService.update((u) {
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
}
FirebaseMessaging.instance.onTokenRefresh
.listen((fcmToken) async {
await UserService.update((u) {
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
})
.onError((err) {
Log.error('could not listen on token refresh');
}); });
} catch (e) { } else {
Log.error('could not load fcm token: $e'); Log.error('Could not update FCM token!');
}
}
} }
}
Future<void> initFCMAfterAuthenticated({bool force = false}) async { static Future<void> resetFCMTokens() async {
final fcmToken = userService.currentUser.fcmToken; await FirebaseInstallations.instance.delete();
if (userService.currentUser.updateFCMToken || force) { Log.info('Firebase Installation successfully deleted.');
if (fcmToken == null) { await FirebaseMessaging.instance.deleteToken();
Log.error('FCM token could not be updated as it is empty'); Log.info('Old FCM deleted.');
await checkForTokenUpdates(); await UserService.update((u) => u.fcmToken = null);
await _checkForTokenUpdates();
await initFCMAfterAuthenticated(force: true);
}
static Future<void> _checkForTokenUpdates() async {
try {
if (!userService.isUserCreated) {
Log.info(
'Checking for FCM token updates skipped: user is not yet created.',
);
return;
}
if (Platform.isIOS) {
var apnsToken = await FirebaseMessaging.instance.getAPNSToken();
for (var i = 0; i < 20; i++) {
if (apnsToken != null) break;
await Future<void>.delayed(const Duration(seconds: 1));
apnsToken = await FirebaseMessaging.instance.getAPNSToken();
}
if (apnsToken == null) {
Log.error('Could not get APNS token even after 20s...');
return;
}
}
final fcmToken = await FirebaseMessaging.instance.getToken();
if (fcmToken == null) {
Log.error('Could not get fcm token');
return;
}
Log.info('Loaded FCM token.');
if (userService.currentUser.fcmToken == null ||
fcmToken != userService.currentUser.fcmToken) {
Log.info('Got new FCM token.');
await UserService.update((u) {
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
if (apiService.isAuthenticated) {
final res = await apiService.updateFCMToken(fcmToken);
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) {
u.updateFCMToken = false;
});
} else {
Log.error('Could not update FCM token!');
}
}
}
FirebaseMessaging.instance.onTokenRefresh
// ignore: avoid_types_on_closure_parameters
.listen((String fcmToken) async {
await UserService.update((u) {
u
..updateFCMToken = true
..fcmToken = fcmToken;
});
if (apiService.isAuthenticated) {
final res = await apiService.updateFCMToken(fcmToken);
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) {
u.updateFCMToken = false;
});
} else {
Log.error('Could not update FCM token!');
}
}
})
.onError((err) {
Log.error('could not listen on token refresh');
});
} catch (e) {
Log.error('could not load fcm token: $e');
}
}
static Future<void> handleRemoteMessage(RemoteMessage message) async {
await _updateLastFcmMessageTimestamp();
if (!Platform.isAndroid) {
Log.error('Got message in Dart while on iOS');
}
if (message.notification != null && AppState.isAppInBackground) {
Log.error(
'Got notification but app is in background, so the SDK already have shown the message.',
);
return; return;
} }
final res = await apiService.updateFCMToken(
fcmToken, if (message.notification != null || message.data['title'] != null) {
); final title =
if (res.isSuccess) { message.notification?.title ?? message.data['title'] as String? ?? '';
Log.info('Uploaded new FCM token!'); final body =
await UserService.update((u) { message.notification?.body ?? message.data['body'] as String? ?? '';
u.updateFCMToken = false; await customLocalPushNotification(title, body);
});
} else {
Log.error('Could not update FCM token!');
} }
} }
}
Future<void> resetFCMTokens() async { static Future<void> _updateLastFcmMessageTimestamp() async {
await FirebaseInstallations.instance.delete(); const storage = FlutterSecureStorage();
Log.info('Firebase Installation successfully deleted.'); final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
await FirebaseMessaging.instance.deleteToken(); try {
Log.info('Old FCM deleted.'); await storage.write(
await UserService.update((u) => u.fcmToken = null); key: SecureStorageKeys.lastFcmMessageTimestamp,
await checkForTokenUpdates(); value: nowMs,
await initFCMAfterAuthenticated(force: true); iOptions: const IOSOptions(
} groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
Future<void> initFCMService() async { ),
await Firebase.initializeApp( );
options: DefaultFirebaseOptions.currentPlatform, Log.info('Updated last FCM message timestamp to $nowMs');
); } catch (e) {
Log.error('Could not write last FCM message timestamp: $e');
unawaited(checkForTokenUpdates());
unawaited(checkFcmHealthAndResetIfNeeded());
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
FirebaseMessaging.onMessage.listen(handleRemoteMessage);
}
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
SentryWidgetsFlutterBinding.ensureInitialized();
await AppEnvironment.init();
final isInitialized = await initBackgroundExecution();
await setupPushNotification();
Log.info('Handling a background message: ${message.messageId}');
await handleRemoteMessage(message);
if (Platform.isAndroid) {
if (isInitialized) {
await handlePeriodicTask(lastExecutionInSecondsLimit: 10);
} }
} else {
// make sure every thing run...
await Future.delayed(const Duration(milliseconds: 2000));
}
}
Future<void> handleRemoteMessage(RemoteMessage message) async {
await updateLastFcmMessageTimestamp();
if (!Platform.isAndroid) {
Log.error('Got message in Dart while on iOS');
}
if (message.notification != null && AppState.isAppInBackground) {
Log.error(
'Got notification but app is in background, so the SDK already have shown the message.',
);
return;
} }
if (message.notification != null || message.data['title'] != null) { static Future<void> updateLastServerMessageTimestamp() async {
final title = const storage = FlutterSecureStorage();
message.notification?.title ?? message.data['title'] as String? ?? ''; final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
final body = try {
message.notification?.body ?? message.data['body'] as String? ?? ''; await storage.write(
await customLocalPushNotification(title, body); key: SecureStorageKeys.lastServerMessageTimestamp,
value: nowMs,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Log.info('Updated last server message timestamp to $nowMs');
} catch (e) {
Log.error('Could not write last server message timestamp: $e');
}
} }
// On Android the push notification is now shown in the server_message.dart. This ensures static Future<void> _checkFcmHealthAndResetIfNeeded() async {
// that the messages was successfully decrypted before showing the push notification if (!userService.isUserCreated) {
// else if (message.data['push_data'] != null) { Log.info('FCM health check skipped: user is not yet created.');
// await handlePushData(message.data['push_data'] as String); return;
// } }
} const storage = FlutterSecureStorage();
try {
final lastFcmStr = await storage.read(
key: SecureStorageKeys.lastFcmMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
final lastServerStr = await storage.read(
key: SecureStorageKeys.lastServerMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Future<void> updateLastFcmMessageTimestamp() async { final now = DateTime.now();
const storage = FlutterSecureStorage(); final threeDaysAgo = now.subtract(const Duration(days: 3));
final nowMs = DateTime.now().millisecondsSinceEpoch.toString();
try {
await storage.write(
key: SecureStorageKeys.lastFcmMessageTimestamp,
value: nowMs,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Log.info('Updated last FCM message timestamp to $nowMs');
} catch (e) {
Log.error('Could not write last FCM message timestamp: $e');
}
}
Future<void> updateLastServerMessageTimestamp() async { DateTime? lastFcmTime;
const storage = FlutterSecureStorage(); if (lastFcmStr != null) {
final nowMs = DateTime.now().millisecondsSinceEpoch.toString(); final ms = int.tryParse(lastFcmStr);
try { if (ms != null) {
await storage.write( lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
key: SecureStorageKeys.lastServerMessageTimestamp, }
value: nowMs,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
Log.info('Updated last server message timestamp to $nowMs');
} catch (e) {
Log.error('Could not write last server message timestamp: $e');
}
}
Future<void> checkFcmHealthAndResetIfNeeded() async {
if (!userService.isUserCreated) return;
const storage = FlutterSecureStorage();
try {
final lastFcmStr = await storage.read(
key: SecureStorageKeys.lastFcmMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
final lastServerStr = await storage.read(
key: SecureStorageKeys.lastServerMessageTimestamp,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
final now = DateTime.now();
final threeDaysAgo = now.subtract(const Duration(days: 3));
DateTime? lastFcmTime;
if (lastFcmStr != null) {
final ms = int.tryParse(lastFcmStr);
if (ms != null) {
lastFcmTime = DateTime.fromMillisecondsSinceEpoch(ms);
} }
}
if (lastFcmTime != null) { if (lastFcmTime != null) {
Log.info('Last message received via FCM messaging system: $lastFcmTime'); Log.info(
} else { 'Last message received via FCM messaging system: $lastFcmTime',
Log.info('No record of a message received via FCM messaging system.'); );
} } else {
Log.info('No record of a message received via FCM messaging system.');
DateTime? lastServerTime;
if (lastServerStr != null) {
final ms = int.tryParse(lastServerStr);
if (ms != null) {
lastServerTime = DateTime.fromMillisecondsSinceEpoch(ms);
} }
}
// Check conditions: DateTime? lastServerTime;
// 1. No messages received via FCM in the last 3 days (either null or older than 3 days) if (lastServerStr != null) {
final fcmInactive = lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo); final ms = int.tryParse(lastServerStr);
// 2. Server message received within the last 3 days if (ms != null) {
final serverActive = lastServerTime != null && lastServerTime.isAfter(threeDaysAgo); lastServerTime = DateTime.fromMillisecondsSinceEpoch(ms);
}
}
if (fcmInactive && serverActive) { final fcmInactive =
Log.warn('FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...'); lastFcmTime == null || lastFcmTime.isBefore(threeDaysAgo);
await resetFCMTokens(); final serverActive =
} else { lastServerTime != null && lastServerTime.isAfter(threeDaysAgo);
Log.info('FCM check passed. No reset needed.');
if (fcmInactive && serverActive) {
Log.warn(
'FCM has been inactive for >3 days, but server messages have been active. Resetting FCM tokens...',
);
await resetFCMTokens();
} else {
Log.info('FCM check passed. No reset needed.');
}
} catch (e) {
Log.error('Error during FCM health check: $e');
} }
} catch (e) {
Log.error('Error during FCM health check: $e');
} }
} }

View file

@ -0,0 +1,640 @@
import 'dart:async';
import 'dart:convert' show base64Url, utf8;
import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart' hide Hmac;
import 'package:cryptography_plus/cryptography_plus.dart'
show Hmac, Mac, SecretBox, SecretKey, Xchacha20;
import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/core/bridge/wrapper.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'
show getContactDisplayName;
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/onboarding_state.model.dart';
import 'package:twonly/src/model/json/userdata.model.dart'
show PasswordLessRecovery;
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/avatars.dart' show getAvatarSvg;
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart';
enum SecondFactorType { email, pin, none }
class PasswordlessRecoveryService {
static final StreamController<String> onEmailTokenReceived =
StreamController<String>.broadcast();
static String linkPrefix = 'https://me.twonly.eu/r/#';
static final Set<String> _handledNotificationIds = {};
static Future<void> handleRecoveryLink(String link) async {
final hashIndex = link.indexOf('#');
if (hashIndex == -1) return;
final fragment = link.substring(hashIndex + 1);
final parts = fragment.split('/');
if (parts.length < 2) {
if (fragment.isNotEmpty) {
onEmailTokenReceived.add(fragment);
final context = rootNavigatorKey.currentContext;
if (context != null && context.mounted) {
unawaited(context.push(Routes.recoverPasswordless, extra: fragment));
}
}
return;
}
final notificationId = parts[0];
final base64Key = parts[1];
if (_handledNotificationIds.contains(notificationId)) {
Log.info(
'Notification ID $notificationId was already handled, skipping.',
);
return;
}
_handledNotificationIds.add(notificationId);
final encryptionKey = base64Url.decode(base64Url.normalize(base64Key));
final context = rootNavigatorKey.currentContext;
if (context != null && context.mounted) {
unawaited(
context.navPush(
HelpAFriendPasswordlessRecoveryView(
notificationId: notificationId,
encryptionKey: encryptionKey,
),
),
);
}
}
static Future<bool> submitRecoveryShare(
String notificationId,
List<int> encryptionKey,
Contact contact,
) async {
try {
final share = contact.recoveryContactsSecretShare;
if (share == null) {
Log.warn('Contact does not have a recovery share stored.');
return false;
}
final trustedFriend = TrustedFriendShare_User(
userId: Int64(userService.currentUser.userId),
displayName: userService.currentUser.displayName,
avatar: userService.currentUser.avatarSvg != null
? utf8.encode(userService.currentUser.avatarSvg!)
: null,
);
final shareUser = TrustedFriendShare_User(
userId: Int64(contact.userId),
displayName: getContactDisplayName(contact),
avatar: contact.avatarSvgCompressed != null
? utf8.encode(getAvatarSvg(contact.avatarSvgCompressed!))
: null,
);
final trustedFriendShare = TrustedFriendShare(
trustedFriend: trustedFriend,
shareUser: shareUser,
threshold: contact.recoveryContactsThreshold,
sharedSecretData: share,
);
final xchacha20 = Xchacha20.poly1305Aead();
final secretBox = await xchacha20.encrypt(
trustedFriendShare.writeToBuffer(),
secretKey: SecretKey(encryptionKey),
nonce: xchacha20.newNonce(),
);
final envelope = EncryptedEnvelope(
encryptedData: secretBox.cipherText,
iv: secretBox.nonce,
mac: secretBox.mac.bytes,
);
final res = await apiService.submitRecoveryShare(
notificationId: notificationId,
encryptedMessage: envelope.writeToBuffer(),
);
return res.isSuccess;
} catch (e) {
Log.error('Failed to submit recovery share', error: e);
return false;
}
}
static Future<bool> enablePasswordlessRecovery({
required List<int> trustedFriendIds,
required SecondFactorType secondFactorType,
required String secondFactorValue,
required int threshold,
}) async {
// 1. Get all currently trusted friends contacts and send them delete messages
final oldTrustedFriends = await (twonlyDB.select(
twonlyDB.contacts,
)..where((t) => t.recoveryIsTrustedFriend.equals(true))).get();
for (final contact in oldTrustedFriends) {
try {
await sendCipherText(
contact.userId,
pb.EncryptedContent(
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
delete: true,
),
),
);
} catch (e) {
Log.error(
'Failed to send delete PasswordLessRecovery message to contact ${contact.userId}: $e',
);
}
}
// 1. Reset current recovery data to ensure a clean state
await twonlyDB.contactsDao.resetRecoveryDataForAllContacts();
await UserService.update((u) => u.passwordLessRecovery = null);
final config = PasswordLessRecovery(threshold);
final xchacha20 = Xchacha20.poly1305Aead();
// 2. If enabled, handle the second factor and create serverKey
Uint8List? serverKey;
Uint8List? encryptedServerKey;
SecretKey? secondFactorEncryptedServerKeyKey;
String? emailHint;
switch (secondFactorType) {
case SecondFactorType.email:
// The serverKey is encrypted with the email; this protects the server from seeing the user's email address, while
// also ensuring that the server can only send the real secret to the user's configured email, as a different
// email will result in a different secret key.
// This is only stored so the user can see there email, and verify that he has set the valid mail...
config.email = secondFactorValue;
emailHint = createEmailHint(secondFactorValue);
// E-Mail Protection:
// - Server can only learn the email during recovery. Ensured as the server gets the NONCE and MAC to decrpyt during recovery.
// - Trusted-friends: Server key is send to the mail, they whould need access to the user's mail account.
secondFactorEncryptedServerKeyKey = SecretKey(
Uint8List.fromList(sha256.convert(utf8.encode(config.email!)).bytes),
);
case SecondFactorType.pin:
// The pin seed - never shared with the server - ensures that the server is unable to brute-force real user's pin.
config.pinSeed = getRandomUint8List(32);
// As the pin is heavily protected against brute-forcing e.g. will be deleted by the server after X tries, the
// unlock token is required to prevent a malicous user (except the trusted friends) to triger this deletion.
config.pinUnlockToken = getRandomUint8List(32);
// Brute-force protection for the user's pin:
// - Server: Does not know the seed.
// - Trusted friends:
// Can only check the result X times before the server deletes the key. As they do not have
// the mac and the cypher text they are unable to brute-force the pin localy. And the server only allows 10
// tries.
final pinProtectionKey = await Hmac.sha256().calculateMac(
Uint8List.fromList(utf8.encode(secondFactorValue)),
secretKey: SecretKey(config.pinSeed!),
);
// To restore the user has to provide the server with this encryption key. The server then can verify the
// correct pin was entered, when decrypting the server key as he also receives the mac and nonce from the user
// during recovery. Only when the mac is correct the server provides the user with the serverKey.
secondFactorEncryptedServerKeyKey = SecretKey(pinProtectionKey.bytes);
case SecondFactorType.none:
}
if (secondFactorEncryptedServerKeyKey != null) {
// The server key is used to encrypt the RecoveryData of the users. This ensures that when the trusted friends
// colaberate, they additional need the serverKey to decrypt the user's key.
serverKey = getRandomUint8List(32);
final secretBox = await xchacha20.encrypt(
serverKey,
secretKey: secondFactorEncryptedServerKeyKey,
nonce: xchacha20.newNonce(),
);
// The server only gets the encrypted server key and the mac. Because the server does not know the nonce (192-bit
// because of XChaCha), he is unable to decrypt the server key without the help of the trusted friends. This
// ensures that the server never learns the users orginal pin, as he is missing the pin_seed and also unable to
// brute-force the email of the user as he does not have the nonce.
encryptedServerKey = Uint8List.fromList([
...secretBox.cipherText,
...secretBox.mac.bytes,
]);
config
..encryptedServerKeyNonce = secretBox.nonce
..encryptedServerKey = encryptedServerKey;
}
// 3. Using shamir's secret to generate the shares for the users.
// 3.1. Create the SharedSecretData
var recoveryData = RecoveryData(
userId: Int64(userService.currentUser.userId),
keyManager: await RustKeyManager.serialize(),
).writeToBuffer();
if (serverKey != null) {
// Second factor was enabled, so encrypt the recoveryData using the serverKey.
final secretBox = await xchacha20.encrypt(
recoveryData,
secretKey: SecretKey(serverKey),
nonce: xchacha20.newNonce(),
);
recoveryData = EncryptedEnvelope(
encryptedData: secretBox.cipherText,
iv: secretBox.nonce,
mac: secretBox.mac.bytes,
).writeToBuffer();
}
final sharedSecretData = SharedSecretData(
recoveryData: recoveryData,
pinSeed: config.pinSeed,
pinUnlockToken: config.pinUnlockToken,
emailHint: emailHint,
encryptedServerKeyNonce: config.encryptedServerKeyNonce,
).writeToBuffer();
// 3.2. Use the amount of trusted friends to generate the shares
final List<Uint8List> shares;
try {
shares = await RustUtils.generateShares(
secret: sharedSecretData,
total: trustedFriendIds.length,
threshold: threshold,
);
if (shares.length != trustedFriendIds.length) {
Log.error('shares.length != trustedFriendIds.length');
return false;
}
} catch (e) {
Log.error('Failed to generate secret shares: $e');
return false;
}
await UserService.update((u) => u.passwordLessRecovery = config);
// 3.4. Store the shares in the contact's rows
for (final contactId in trustedFriendIds) {
await twonlyDB.contactsDao.updateContact(
contactId,
ContactsCompanion(
recoveryIsTrustedFriend: const Value(true),
recoveryLastHeartbeat: const Value(null),
recoverySecretShare: Value(shares.removeLast()),
),
);
}
unawaited(performHeartbeat());
// The passwordless is configured sucessfully.
return true;
}
static Future<bool> testPin(String pin) async {
final config = userService.currentUser.passwordLessRecovery;
if (config?.pinSeed == null || config?.encryptedServerKey == null) {
return false;
}
try {
final pinProtectionKey = await Hmac.sha256().calculateMac(
Uint8List.fromList(utf8.encode(pin)),
secretKey: SecretKey(config!.pinSeed!),
);
final secondFactorEncryptedServerKeyKey = SecretKey(
pinProtectionKey.bytes,
);
final xchacha20 = Xchacha20.poly1305Aead();
final combined = config.encryptedServerKey!;
final cipherText = combined.sublist(0, combined.length - 16);
final macBytes = combined.sublist(combined.length - 16);
final secretBox = SecretBox(
cipherText,
nonce: config.encryptedServerKeyNonce!,
mac: Mac(macBytes),
);
await xchacha20.decrypt(
secretBox,
secretKey: secondFactorEncryptedServerKeyKey,
);
return true;
} catch (e) {
Log.error('Failed to test pin: $e');
return false;
}
}
static Future<void> performHeartbeat() async {
final config = userService.currentUser.passwordLessRecovery;
if (config != null) {
final lastHeartbeat = config.lastServerHeartbeat;
final isOlderThanAMonth =
lastHeartbeat != null &&
clock.now().difference(lastHeartbeat).inDays > 20;
if ((lastHeartbeat == null || isOlderThanAMonth) &&
config.encryptedServerKey != null) {
final res = await apiService.registerPasswordLessRecovery(
config.encryptedServerKey!,
config.pinUnlockToken,
);
if (res.isSuccess) {
await UserService.update((u) {
u.passwordLessRecovery?.lastServerHeartbeat = clock.now();
});
}
}
final lastContactHeartbeat = config.lastContactHeartbeat;
final isContactHeartbeatOlderThan24h =
lastContactHeartbeat == null ||
clock.now().difference(lastContactHeartbeat).inHours >= 24;
if (isContactHeartbeatOlderThan24h) {
// Get all contacts where recoveryLastHeartbeat is NULL. Then for each contacts send.
// recoveryLastHeartbeat is ONLY updated in case the contact has responded.
final pendingShares =
await (twonlyDB.select(twonlyDB.contacts)..where(
(t) =>
t.recoveryIsTrustedFriend.equals(true) &
t.recoveryLastHeartbeat.isNull() &
t.recoverySecretShare.isNotNull(),
))
.get();
for (final contact in pendingShares) {
try {
await sendCipherText(
contact.userId,
pb.EncryptedContent(
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
recoverySecretShare: contact.recoverySecretShare,
delete: false,
threshold: Int64(config.threshold),
),
),
);
} catch (e) {
Log.error(
'Failed to send PasswordLessRecovery share to contact ${contact.userId}: $e',
);
}
}
await UserService.update((u) {
u.passwordLessRecovery?.lastContactHeartbeat = clock.now();
});
}
}
// Send heartbeat to the friends I am a trusted friend.
final oneWeekAgo = clock.now().subtract(const Duration(days: 7));
final trustedFriendsToNotify =
await (twonlyDB.select(twonlyDB.contacts)..where(
(t) =>
t.recoveryContactsSecretShare.isNotNull() &
(t.recoveryContactsLastHeartbeat.isNull() |
t.recoveryContactsLastHeartbeat.isSmallerThanValue(
oneWeekAgo,
)),
))
.get();
for (final contact in trustedFriendsToNotify) {
try {
final share = contact.recoveryContactsSecretShare!;
final hash = sha256.convert(share).bytes;
await sendCipherText(
contact.userId,
pb.EncryptedContent(
passwordlessRecoveryHeartbeat:
pb.EncryptedContent_PasswordLessRecoveryHeartbeat(
hash: hash,
),
),
);
await twonlyDB.contactsDao.updateContact(
contact.userId,
ContactsCompanion(
recoveryContactsLastHeartbeat: Value(clock.now()),
),
);
} catch (e) {
Log.error(
'Failed to send PasswordLessRecoveryHeartbeat to contact ${contact.userId}: $e',
);
}
}
}
static Future<void> handlePasswordlessRecovery(
int fromUserId,
pb.EncryptedContent_PasswordLessRecovery msg,
String receiptId,
) async {
if (msg.delete) {
Log.info(
'[$receiptId] Received request to delete passwordless recovery share from contact $fromUserId',
);
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(
recoveryContactsSecretShare: Value(null),
recoveryContactsLastHeartbeat: Value(null),
),
);
} else if (msg.hasRecoverySecretShare() && msg.hasThreshold()) {
Log.info(
'[$receiptId] Received new passwordless recovery share from contact $fromUserId',
);
await twonlyDB.contactsDao.updateContact(
fromUserId,
ContactsCompanion(
recoveryContactsSecretShare: Value(
Uint8List.fromList(msg.recoverySecretShare),
),
recoveryContactsThreshold: Value(msg.threshold.toInt()),
recoveryContactsLastHeartbeat: const Value(
null, // this will trigger that a heartbeat will be send...
),
),
);
}
unawaited(performHeartbeat());
}
static Future<void> handlePasswordlessRecoveryHeartbeat(
int fromUserId,
pb.EncryptedContent_PasswordLessRecoveryHeartbeat msg,
String receiptId,
) async {
Log.info(
'[$receiptId] Received passwordless recovery heartbeat from contact $fromUserId',
);
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
final storedShare = contact?.recoverySecretShare;
if (storedShare == null) {
unawaited(
sendCipherText(
fromUserId,
pb.EncryptedContent(
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
delete: true,
),
),
),
);
Log.warn(
'[$receiptId] Received passwordless recovery heartbeat from $fromUserId but we did not send him a secret share.',
);
return;
}
final computedHash = sha256.convert(storedShare).bytes;
final recoveryLastHeartbeat =
const ListEquality().equals(computedHash, msg.hash)
? clock.now()
: null; // The stored share not valid (maybe a old backup was restored). This will cause the performHeartbeat to resend him his share
Log.info(
'[$receiptId] Got heartbeat: ($recoveryLastHeartbeat)',
);
await twonlyDB.contactsDao.updateContact(
fromUserId,
ContactsCompanion(
recoveryLastHeartbeat: Value(recoveryLastHeartbeat),
),
);
}
static Future<bool> checkAndStorePasswordlessMessages(
OnboardingState state,
) async {
if (!state.serverRegistered ||
state.notificationId == null ||
state.downloadAuthToken == null ||
state.encryptionKey == null) {
return false;
}
final alreadyReceivedIds = state.receivedShares
.map((s) => Int64(s.messageId))
.toList();
final response = await apiService.checkForPasswordlessNotification(
notificationId: state.notificationId!,
downloadAuthToken: state.downloadAuthToken!,
alreadyReceivedIds: alreadyReceivedIds,
);
if (response == null || response.messages.isEmpty) {
return false;
}
final xchacha20 = Xchacha20.poly1305Aead();
final secretKey = SecretKey(state.encryptionKey!);
var didUpdate = false;
for (final msg in response.messages) {
final msgId = msg.id.toInt();
try {
final envelope = EncryptedEnvelope.fromBuffer(msg.encryptedMessage);
final secretBox = SecretBox(
envelope.encryptedData,
nonce: envelope.iv,
mac: Mac(envelope.mac),
);
final plaintext = await xchacha20.decrypt(
secretBox,
secretKey: secretKey,
);
final share = TrustedFriendShare.fromBuffer(plaintext);
final receivedShare = ReceivedRecoveryShare(
messageId: msgId,
trustedFriendDisplayName: share.trustedFriend.displayName,
myDisplayName: share.shareUser.displayName,
myUserId: share.shareUser.userId.toInt(),
myAvatarSvg: share.shareUser.hasAvatar()
? share.shareUser.avatar
: null,
threshold: share.threshold,
sharedSecretDataBytes: share.sharedSecretData,
);
state.receivedShares.add(receivedShare);
didUpdate = true;
Log.info(
'Received recovery share from ${share.trustedFriend.displayName} '
'for user ${share.shareUser.displayName}',
);
} catch (e) {
Log.error(
'Failed to decrypt/parse passwordless notification message $msgId: $e',
);
}
}
if (didUpdate) {
await KeyValueStore.update<OnboardingState>(
key: KeyValueKeys.onboardingState,
update: (s) => s.receivedShares = state.receivedShares,
);
}
return didUpdate;
}
}

View file

@ -1,8 +1 @@
enum SetupProfile { standard, customized, maximum } enum SetupProfile { standard, customized }
enum SecurityProfile { normal, strict }
extension SecurityProfileExtension on SecurityProfile {
bool get showWarningForNonVerifiedContacts => this == SecurityProfile.strict;
bool get showOnlyVerifiedInChatViewList => this == SecurityProfile.normal;
}

View file

@ -29,7 +29,7 @@ Future<CiphertextMessage?> _signalEncryptMessage(
final session = SessionCipher.fromStore(signalStore, address); final session = SessionCipher.fromStore(signalStore, address);
return await session.encrypt(plaintextContent); return await session.encrypt(plaintextContent);
} catch (e) { } catch (e) {
Log.error(e.toString()); Log.error('Could not encrypt message for target $target: $e');
return null; return null;
} }
} }
@ -38,8 +38,9 @@ Future<(EncryptedContent?, PlaintextContent_DecryptionErrorMessage_Type?)>
signalDecryptMessage( signalDecryptMessage(
int fromUserId, int fromUserId,
Uint8List encryptedContentRaw, Uint8List encryptedContentRaw,
int type, int type, {
) async { Set<int>? brokenSessionsInCurrentBatch,
}) async {
// Hold the lock only for the cryptographic operation, not for network I/O // Hold the lock only for the cryptographic operation, not for network I/O
Log.info('Acquiring lockingSignalProtocol for $fromUserId'); Log.info('Acquiring lockingSignalProtocol for $fromUserId');
final ( final (
@ -74,6 +75,7 @@ signalDecryptMessage(
); );
} }
recordResyncAttempt(fromUserId, success: true);
return (EncryptedContent.fromBuffer(plaintext), null, false); return (EncryptedContent.fromBuffer(plaintext), null, false);
} on InvalidKeyIdException catch (e) { } on InvalidKeyIdException catch (e) {
Log.warn(e); Log.warn(e);
@ -113,22 +115,25 @@ signalDecryptMessage(
// Handle session resync OUTSIDE the lock to avoid holding it during // Handle session resync OUTSIDE the lock to avoid holding it during
// network round-trips (which can block for up to 60 seconds) // network round-trips (which can block for up to 60 seconds)
if (needsResync && !resyncedUsers.contains(fromUserId)) { if (needsResync) {
if (await handleSessionResync(fromUserId)) { brokenSessionsInCurrentBatch?.add(fromUserId);
// This flag prevents from resyncing the session the client received if (shouldAttemptResync(fromUserId)) {
// multiple new messages from the server he could not decrypt if (await handleSessionResync(fromUserId)) {
resyncedUsers.add(fromUserId); // This flag prevents from resyncing the session the client received
// multiple new messages from the server he could not decrypt
recordResyncAttempt(fromUserId, success: false);
// This message contains a new PreKeyBundle establishing a new signal // This message contains a new PreKeyBundle establishing a new signal
// session // session
await sendCipherText( await sendCipherText(
fromUserId, fromUserId,
EncryptedContent( EncryptedContent(
errorMessages: EncryptedContent_ErrorMessages( errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC, type: EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC,
),
), ),
), );
); }
} }
} }

View file

@ -1,12 +1,36 @@
import 'dart:math';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
/// Unified lock for all Signal protocol operations (encryption, decryption, session management). /// Unified lock for all Signal protocol operations (encryption, decryption, session management).
final lockingSignalProtocol = Mutex(); final lockingSignalProtocol = Mutex();
/// Tracking users who have already been resynced in the current session. /// Tracking users who have already been resynced in the current session.
final resyncedUsers = <int>{}; final Map<int, ({int failureCount, DateTime lastAttempt})> _resyncAttempts = {};
/// Reset the resync tracking set. const int maxResyncAttempts = 3;
void resetResyncedUsers() {
resyncedUsers.clear(); bool shouldAttemptResync(int userId) {
final attempt = _resyncAttempts[userId];
if (attempt == null) return true;
if (attempt.failureCount >= maxResyncAttempts) return false;
final cooldown = Duration(minutes: 5 * pow(5, attempt.failureCount - 1).toInt());
return DateTime.now().difference(attempt.lastAttempt) > cooldown;
}
void recordResyncAttempt(int userId, {required bool success}) {
if (success) {
_resyncAttempts.remove(userId);
} else {
final current = _resyncAttempts[userId];
_resyncAttempts[userId] = (
failureCount: (current?.failureCount ?? 0) + 1,
lastAttempt: DateTime.now(),
);
}
}
/// Reset the resync tracking set (currently unused, backoff handles expiry naturally).
void resetResyncedUsers() {
// No-op. We want the backoff state to persist across reconnects.
} }

View file

@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
@ -128,6 +130,12 @@ Future<void> handleUserStudyUpload() async {
}); });
} }
} catch (e) { } catch (e) {
Log.error(e); if (e is http.ClientException ||
e is SocketException ||
e is TimeoutException) {
Log.warn('Error uploading user study data: $e');
} else {
Log.error(e);
}
} }
} }

View file

@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/utils/log.dart';
String getAvatarSvg(Uint8List avatarSvgCompressed) { String getAvatarSvg(Uint8List avatarSvgCompressed) {
return utf8.decode(gzip.decode(avatarSvgCompressed)); return utf8.decode(gzip.decode(avatarSvgCompressed));
@ -16,28 +17,35 @@ Future<void> createPushAvatars({int? forceForUserId}) async {
final contacts = await twonlyDB.contactsDao.getAllContacts(); final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) { for (final contact in contacts) {
if (contact.avatarSvgCompressed == null) continue; try {
if (contact.avatarSvgCompressed == null) continue;
if (forceForUserId == null) { if (forceForUserId == null) {
if (avatarPNGFile(contact.userId).existsSync()) { if (avatarPNGFile(contact.userId).existsSync()) {
continue; // only create the avatar in case no avatar exists yet fot this user continue; // only create the avatar in case no avatar exists yet fot this user
}
} else if (contact.userId != forceForUserId) {
// only update the avatar for this specified contact
continue;
} }
} else if (contact.userId != forceForUserId) {
// only update the avatar for this specified contact final avatarSvg = getAvatarSvg(contact.avatarSvgCompressed!);
continue;
final pictureInfo = await vg.loadPicture(
SvgStringLoader(avatarSvg),
null,
);
final image = await pictureInfo.picture.toImage(270, 300);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List();
await avatarPNGFile(contact.userId).writeAsBytes(pngBytes);
pictureInfo.picture.dispose();
} catch (e) {
Log.error(e);
} }
final avatarSvg = getAvatarSvg(contact.avatarSvgCompressed!);
final pictureInfo = await vg.loadPicture(SvgStringLoader(avatarSvg), null);
final image = await pictureInfo.picture.toImage(270, 300);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List();
await avatarPNGFile(contact.userId).writeAsBytes(pngBytes);
pictureInfo.picture.dispose();
} }
} }
@ -52,10 +60,26 @@ File avatarPNGFile(int contactId) {
return File('${avatarsDirectory.path}/$contactId.png'); return File('${avatarsDirectory.path}/$contactId.png');
} }
Future<Uint8List> getUserAvatar() async { File currentUserAvatarFile(int avatarCounter) {
final avatarsDirectory = Directory(
'${AppEnvironment.cacheDir}/avatars',
);
if (!avatarsDirectory.existsSync()) {
avatarsDirectory.createSync(recursive: true);
}
return File('${avatarsDirectory.path}/user_$avatarCounter.png');
}
Future<String?> getUserAvatar() async {
if (userService.currentUser.avatarSvg == null) { if (userService.currentUser.avatarSvg == null) {
final data = await rootBundle.load('assets/images/default_avatar.png'); return null;
return data.buffer.asUint8List(); }
final avatarCounter = userService.currentUser.avatarCounter;
final file = currentUserAvatarFile(avatarCounter);
if (file.existsSync()) {
return file.path;
} }
final pictureInfo = await vg.loadPicture( final pictureInfo = await vg.loadPicture(
@ -68,9 +92,8 @@ Future<Uint8List> getUserAvatar() async {
final byteData = await image.toByteData(format: ui.ImageByteFormat.png); final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final pngBytes = byteData!.buffer.asUint8List(); final pngBytes = byteData!.buffer.asUint8List();
final file = avatarPNGFile(userService.currentUser.userId) await file.writeAsBytes(pngBytes);
..writeAsBytesSync(pngBytes);
pictureInfo.picture.dispose(); pictureInfo.picture.dispose();
return file.readAsBytesSync(); return file.path;
} }

View file

@ -1,12 +1,39 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/json/backup.model.dart';
import 'package:twonly/src/model/json/onboarding_state.model.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
typedef ModelFactory = ({
Object Function(Map<String, dynamic> json) fromJson,
Map<String, dynamic> Function(Object value) toJson,
Object Function() defaultValue,
});
class KeyValueStore { class KeyValueStore {
static final Map<Type, ModelFactory> _registry = {
OnboardingState: (
fromJson: OnboardingState.fromJson,
toJson: (val) => (val as OnboardingState).toJson(),
defaultValue: OnboardingState.new,
),
CurrentBackupStatus: (
fromJson: CurrentBackupStatus.fromJson,
toJson: (val) => (val as CurrentBackupStatus).toJson(),
defaultValue: CurrentBackupStatus.new,
),
BackupRecovery: (
fromJson: BackupRecovery.fromJson,
toJson: (val) => (val as BackupRecovery).toJson(),
defaultValue: () => BackupRecovery(username: '', password: '', userId: 0),
),
};
static final Map<String, Mutex> _mutexes = {}; static final Map<String, Mutex> _mutexes = {};
static Mutex _getMutex(String key) { static Mutex _getMutex(String key) {
@ -65,4 +92,77 @@ class KeyValueStore {
} }
}); });
} }
static Future<T> update<T>({
required String key,
required FutureOr<void> Function(T value) update,
}) async {
final factory = _registry[T];
if (factory == null) {
throw ArgumentError('Type $T is not registered in KeyValueStore.');
}
return _exclusive(key, () async {
T val;
final file = await _getFilePath(key);
try {
if (file.existsSync()) {
final contents = await file.readAsString();
val = factory.fromJson(jsonDecode(contents) as Map<String, dynamic>) as T;
} else {
val = factory.defaultValue() as T;
}
} catch (e) {
Log.warn('Error reading file. Resetting to default.: $e');
val = factory.defaultValue() as T;
}
await update(val);
try {
await file.parent.create(recursive: true);
await file.writeAsString(jsonEncode(factory.toJson(val as Object)));
} catch (e) {
Log.error('Error writing file: $e');
}
return val;
});
}
static Future<T> getModel<T>(String key) async {
final factory = _registry[T];
if (factory == null) {
throw ArgumentError('Type $T is not registered in KeyValueStore.');
}
return _exclusive(key, () async {
final file = await _getFilePath(key);
try {
if (file.existsSync()) {
final contents = await file.readAsString();
return factory.fromJson(jsonDecode(contents) as Map<String, dynamic>) as T;
}
} catch (e) {
Log.warn('Error reading file. Returning default.: $e');
}
return factory.defaultValue() as T;
});
}
static Future<T?> getModelOrNull<T>(String key) async {
final factory = _registry[T];
if (factory == null) {
throw ArgumentError('Type $T is not registered in KeyValueStore.');
}
return _exclusive(key, () async {
final file = await _getFilePath(key);
try {
if (file.existsSync()) {
final contents = await file.readAsString();
return factory.fromJson(jsonDecode(contents) as Map<String, dynamic>) as T;
}
} catch (e) {
Log.warn('Error reading file.: $e');
}
return null;
});
}
} }

View file

@ -41,10 +41,14 @@ class Log {
} }
static void error( static void error(
Object? messageInput, [ Object? messageInput, {
Object? error, Object? error,
StackTrace? stackTrace, StackTrace? stackTrace,
]) { bool onlyIfSentryEnabled = false,
}) {
if (!AppState.allowErrorTrackingViaSentry && onlyIfSentryEnabled) {
return;
}
final message = filterLogMessage('$messageInput'); final message = filterLogMessage('$messageInput');
if (AppState.allowErrorTrackingViaSentry) { if (AppState.allowErrorTrackingViaSentry) {
try { try {

View file

@ -11,10 +11,12 @@ import 'package:gal/gal.dart';
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:path/path.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -36,6 +38,7 @@ extension ShortCutsExtension on BuildContext {
Future<String?> saveImageToGallery( Future<String?> saveImageToGallery(
Uint8List imageBytes, { Uint8List imageBytes, {
DateTime? createdAt, DateTime? createdAt,
String? name,
}) async { }) async {
var bytesToProcess = imageBytes; var bytesToProcess = imageBytes;
@ -75,7 +78,7 @@ Future<String?> saveImageToGallery(
await Gal.requestAccess(toAlbum: true); await Gal.requestAccess(toAlbum: true);
} }
try { try {
await Gal.putImageBytes(jpgImages, album: 'twonly'); await Gal.putImageBytes(jpgImages, album: 'twonly', name: name ?? 'image');
return null; return null;
} on GalException catch (e) { } on GalException catch (e) {
Log.error(e); Log.error(e);
@ -83,17 +86,45 @@ Future<String?> saveImageToGallery(
} }
} }
Future<String?> saveVideoToGallery(String videoPath) async { Future<String?> saveVideoToGallery(
String videoPath, {
String? name,
}) async {
final hasAccess = await Gal.hasAccess(toAlbum: true); final hasAccess = await Gal.hasAccess(toAlbum: true);
if (!hasAccess) { if (!hasAccess) {
await Gal.requestAccess(toAlbum: true); await Gal.requestAccess(toAlbum: true);
} }
var pathToSave = videoPath;
File? tempFile;
try { try {
await Gal.putVideo(videoPath, album: 'twonly'); if (name != null) {
final file = File(videoPath);
final extension = file.path.split('.').last;
final tempDir = Directory.systemTemp;
tempFile = File(join(tempDir.path, '$name.$extension'));
if (tempFile.existsSync()) {
try {
tempFile.deleteSync();
} catch (_) {}
}
file.copySync(tempFile.path);
pathToSave = tempFile.path;
}
await Gal.putVideo(pathToSave, album: 'twonly');
return null; return null;
} on GalException catch (e) { } on GalException catch (e) {
Log.error(e); Log.error(e);
return e.type.message; return e.type.message;
} finally {
if (tempFile != null && tempFile.existsSync()) {
try {
tempFile.deleteSync();
} catch (e) {
Log.error('Failed to delete temp video file: $e');
}
}
} }
} }
@ -375,3 +406,43 @@ String joinWithAnd(List<String> items, String andWord) {
if (items.length == 1) return items.first; if (items.length == 1) return items.first;
return '${items.sublist(0, items.length - 1).join(', ')} $andWord ${items.last}'; return '${items.sublist(0, items.length - 1).join(', ')} $andWord ${items.last}';
} }
String createEmailHint(String email) {
final parts = email.split('@');
if (parts.length != 2) return email;
final local = parts[0];
final domain = parts[1];
final localMasked = local.length > 2
? '${local[0]}${'*' * (local.length - 2)}${local[local.length - 1]}'
: local;
final domainParts = domain.split('.');
if (domainParts.isEmpty) return '$localMasked@$domain';
final domainName = domainParts[0];
final domainNameMasked = domainName.length > 1
? '${domainName[0]}${'*' * (domainName.length - 1)}'
: domainName;
final restDomain = domainParts.skip(1).join('.');
return '$localMasked@$domainNameMasked${restDomain.isNotEmpty ? '.$restDomain' : ''}';
}
extension RecoveryErrorLocalization on RecoveryError {
String toLocalizedString(BuildContext context) {
switch (this) {
case RecoveryError.noInternet:
return context.lang.recoverErrorNoInternet;
case RecoveryError.usernameNotValid:
return context.lang.recoverErrorUsernameNotValid;
case RecoveryError.passwordInvalid:
return context.lang.recoverErrorPasswordInvalid;
case RecoveryError.tryAgainLater:
return context.lang.recoverErrorTryAgainLater;
case RecoveryError.unkownError:
return context.lang.recoverErrorUnknown;
}
}
}

Some files were not shown because too many files have changed in this diff Show more