diff --git a/flutter_rust_bridge.yaml b/flutter_rust_bridge.yaml index 30bd38a7..0db58980 100644 --- a/flutter_rust_bridge.yaml +++ b/flutter_rust_bridge.yaml @@ -1,3 +1,3 @@ rust_input: crate::bridge rust_root: rust -dart_output: lib/core \ No newline at end of file +dart_output: lib/core diff --git a/lib/app.dart b/lib/app.dart index c383e546..c1c68482 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -220,7 +220,11 @@ class _AppMainWidgetState extends State { } else { // This means the user is in the onboarding screen, so start with the Proof of Work. - final (proof, disabled) = await apiService.getProofOfWork(); + final proofResult = await rustApiResult(RustApi.getProofOfWork()); + final proof = proofResult.value == null + ? null + : decodeProofOfWork(proofResult.value!); + final disabled = proofResult.error == ErrorCode.RegistrationDisabled; if (proof != null) { Log.info('Starting with proof of work calculation.'); _proofOfWork = ( diff --git a/lib/core/api/proto/client/encrypted_content.dart b/lib/core/api/proto/client/encrypted_content.dart new file mode 100644 index 00000000..8d69d722 --- /dev/null +++ b/lib/core/api/proto/client/encrypted_content.dart @@ -0,0 +1,32 @@ +// 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 PasswordLessRecovery { + final Uint8List? recoverySecretShare; + final bool delete; + final PlatformInt64 threshold; + + const PasswordLessRecovery({ + this.recoverySecretShare, + required this.delete, + required this.threshold, + }); + + @override + int get hashCode => + recoverySecretShare.hashCode ^ delete.hashCode ^ threshold.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PasswordLessRecovery && + runtimeType == other.runtimeType && + recoverySecretShare == other.recoverySecretShare && + delete == other.delete && + threshold == other.threshold; +} diff --git a/lib/core/bridge/api.dart b/lib/core/bridge/api.dart index 9ce3f7cf..8d15fe59 100644 --- a/lib/core/bridge/api.dart +++ b/lib/core/bridge/api.dart @@ -5,15 +5,11 @@ import '../api/server/prekeys.dart'; import '../frb_generated.dart'; - import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -import 'package:freezed_annotation/freezed_annotation.dart' hide protected; -part 'api.freezed.dart'; -// These functions are ignored because they are not marked as `pub`: `from_rust_state` +// These functions are ignored because they are not marked as `pub`: `api_result`, `empty_api_response`, `encoded_api_response`, `from_rust_state` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ApiConfig`, `ServerResult` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt` -// These functions are ignored (category: IgnoreBecauseOwnerTyShouldIgnore): `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge` enum ApiConnectionState { stopped, @@ -85,11 +81,10 @@ class PreparedOutgoingMessage { class RustApi { const RustApi(); - static Future addAdditionalUser({ - required PlatformInt64 userId, - }) => RustLib.instance.api.crateBridgeApiRustApiAddAdditionalUser( - userId: userId, - ); + static Future addAdditionalUser({required PlatformInt64 userId}) => + RustLib.instance.api.crateBridgeApiRustApiAddAdditionalUser( + userId: userId, + ); static Future allocateSequence() => RustLib.instance.api.crateBridgeApiRustApiAllocateSequence(); @@ -97,15 +92,15 @@ class RustApi { static String apiBaseUrl({required String protocol}) => RustLib.instance.api.crateBridgeApiRustApiApiBaseUrl(protocol: protocol); - static Future changeUsername({required String username}) => - RustLib.instance.api.crateBridgeApiRustApiChangeUsername( - username: username, - ); + static Future changeUsername({required String username}) => RustLib + .instance + .api + .crateBridgeApiRustApiChangeUsername(username: username); static Future checkForDeletedUsernames() => RustLib.instance.api.crateBridgeApiRustApiCheckForDeletedUsernames(); - static Future checkForPasswordlessNotification({ + static Future checkForPasswordlessNotification({ required String notificationId, required List downloadAuthToken, required Int64List alreadyReceivedMessageIds, @@ -119,11 +114,10 @@ class RustApi { static Future close() => RustLib.instance.api.crateBridgeApiRustApiClose(); - static Future confirmMemoriesUpload({ - required String mediaId, - }) => RustLib.instance.api.crateBridgeApiRustApiConfirmMemoriesUpload( - mediaId: mediaId, - ); + static Future confirmMemoriesUpload({required String mediaId}) => + RustLib.instance.api.crateBridgeApiRustApiConfirmMemoriesUpload( + mediaId: mediaId, + ); static Future connect() => RustLib.instance.api.crateBridgeApiRustApiConnect(); @@ -131,25 +125,31 @@ class RustApi { static Future connectionState() => RustLib.instance.api.crateBridgeApiRustApiConnectionState(); - static Future deleteAccount() => + static Future deleteAccount() => RustLib.instance.api.crateBridgeApiRustApiDeleteAccount(); - static Future deleteMemory({required String mediaId}) => + static Future deleteMemory({required String mediaId}) => RustLib.instance.api.crateBridgeApiRustApiDeleteMemory(mediaId: mediaId); - static Future disableMemoriesBackup() => + static Future disableMemoriesBackup() => RustLib.instance.api.crateBridgeApiRustApiDisableMemoriesBackup(); - static Future downloadDone({required List token}) => + static Future downloadDone({required List token}) => RustLib.instance.api.crateBridgeApiRustApiDownloadDone(token: token); + static Future downloadMedia({required String mediaId}) => + RustLib.instance.api.crateBridgeApiRustApiDownloadMedia(mediaId: mediaId); + + static Future downloadPendingMedia() => + RustLib.instance.api.crateBridgeApiRustApiDownloadPendingMedia(); + static Stream events() => RustLib.instance.api.crateBridgeApiRustApiEvents(); - static Future forceIpaCheck() => + static Future forceIpaCheck() => RustLib.instance.api.crateBridgeApiRustApiForceIpaCheck(); - static Future getMemoriesUrl({ + static Future getMemoriesUrl({ required String mediaId, required bool thumbnail, }) => RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUrl( @@ -157,16 +157,16 @@ class RustApi { thumbnail: thumbnail, ); - static Future getMemoriesUsage() => + static Future getMemoriesUsage() => RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUsage(); static Future getPlanBalance() => RustLib.instance.api.crateBridgeApiRustApiGetPlanBalance(); - static Future getProofOfWork() => + static Future getProofOfWork() => RustLib.instance.api.crateBridgeApiRustApiGetProofOfWork(); - static Future getServerKeyForPasswordlessRecovery({ + static Future getServerKeyForPasswordlessRecovery({ required PlatformInt64 userId, required List serverKeyProtection, Uint8List? pinUnlockToken, @@ -181,14 +181,13 @@ class RustApi { email: email, ); - static Future getUserById({ - required PlatformInt64 userId, - }) => RustLib.instance.api.crateBridgeApiRustApiGetUserById(userId: userId); + static Future getUserById({required PlatformInt64 userId}) => + RustLib.instance.api.crateBridgeApiRustApiGetUserById(userId: userId); - static Future getUserData({required String username}) => + static Future getUserData({required String username}) => RustLib.instance.api.crateBridgeApiRustApiGetUserData(username: username); - static Future getUserIdFromUsername({ + static Future getUserIdFromUsername({ required String username, }) => RustLib.instance.api.crateBridgeApiRustApiGetUserIdFromUsername( username: username, @@ -230,7 +229,7 @@ class RustApi { quoteMessageId: quoteMessageId, ); - static Future ipaPurchase({ + static Future ipaPurchase({ required String productId, required String source, required String verificationData, @@ -240,10 +239,8 @@ class RustApi { verificationData: verificationData, ); - static Future loadPlanBalance({required bool useCache}) => RustLib - .instance - .api - .crateBridgeApiRustApiLoadPlanBalance(useCache: useCache); + static Future loadPlanBalance() => + RustLib.instance.api.crateBridgeApiRustApiLoadPlanBalance(); static Future notifyMessagesOpened({ required PlatformInt64 contactId, @@ -253,13 +250,18 @@ class RustApi { messageIds: messageIds, ); + static Future performPasswordlessRecoveryHeartbeat() => RustLib + .instance + .api + .crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat(); + static Future prepareQueuedMessage({ required String receiptId, }) => RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage( receiptId: receiptId, ); - static Future register({ + static Future register({ required String username, required PlatformInt64 proofOfWork, required String langCode, @@ -271,7 +273,7 @@ class RustApi { isIos: isIos, ); - static Future registerPasswordlessNotification({ + static Future registerPasswordlessNotification({ required String notificationId, required List downloadAuthToken, required String langCode, @@ -284,7 +286,7 @@ class RustApi { googleFcm: googleFcm, ); - static Future registerPasswordlessRecovery({ + static Future registerPasswordlessRecovery({ required List encryptedServerKey, Uint8List? pinUnlockToken, }) => RustLib.instance.api.crateBridgeApiRustApiRegisterPasswordlessRecovery( @@ -297,13 +299,12 @@ class RustApi { static Future reloadConfiguration() => RustLib.instance.api.crateBridgeApiRustApiReloadConfiguration(); - static Future removeAdditionalUser({ - required PlatformInt64 userId, - }) => RustLib.instance.api.crateBridgeApiRustApiRemoveAdditionalUser( - userId: userId, - ); + static Future removeAdditionalUser({required PlatformInt64 userId}) => + RustLib.instance.api.crateBridgeApiRustApiRemoveAdditionalUser( + userId: userId, + ); - static Future reportUser({ + static Future reportUser({ required PlatformInt64 userId, required String reason, }) => RustLib.instance.api.crateBridgeApiRustApiReportUser( @@ -321,7 +322,12 @@ class RustApi { username: username, ); - static Future requestMemoriesUpload({ + static Future requestMediaReupload({required String mediaId}) => RustLib + .instance + .api + .crateBridgeApiRustApiRequestMediaReupload(mediaId: mediaId); + + static Future requestMemoriesUpload({ required PlatformInt64 size, required PlatformInt64 originalDate, required String mediaId, @@ -375,7 +381,7 @@ class RustApi { .api .crateBridgeApiRustApiSendQueuedMessage(receiptId: receiptId); - static Future sendTextMessage({ + static Future sendTextMessage({ required PlatformInt64 userId, required List body, Uint8List? pushData, @@ -398,7 +404,7 @@ class RustApi { .api .crateBridgeApiRustApiSetBackground(inBackground: inBackground); - static Future setLoginToken({required List token}) => + static Future setLoginToken({required List token}) => RustLib.instance.api.crateBridgeApiRustApiSetLoginToken(token: token); static Future setNetworkAvailable({required bool available}) => RustLib @@ -406,7 +412,7 @@ class RustApi { .api .crateBridgeApiRustApiSetNetworkAvailable(available: available); - static Future submitRecoveryShare({ + static Future submitRecoveryShare({ required String notificationId, required List encryptedMessage, }) => RustLib.instance.api.crateBridgeApiRustApiSubmitRecoveryShare( @@ -414,10 +420,10 @@ class RustApi { encryptedMessage: encryptedMessage, ); - static Future updateFcmToken({required String token}) => + static Future updateFcmToken({required String token}) => RustLib.instance.api.crateBridgeApiRustApiUpdateFcmToken(token: token); - static Future updateSignedPreKey({ + static Future updateSignedPreKey({ required PlatformInt64 id, required List key, required List signature, @@ -427,7 +433,7 @@ class RustApi { signature: signature, ); - static Future uploadPqcPreKeys({ + static Future uploadPqcPreKeys({ required PlatformInt64 eccSignedPrekeyId, required List eccSignedPrekey, required List eccSignedPrekeySignature, @@ -453,37 +459,3 @@ class RustApi { identical(this, other) || other is RustApi && runtimeType == other.runtimeType; } - -@freezed -sealed class ServerResultEmpty with _$ServerResultEmpty { - const ServerResultEmpty._(); - - const factory ServerResultEmpty.ok() = ServerResultEmpty_Ok; - const factory ServerResultEmpty.errorCode( - int field0, - ) = ServerResultEmpty_ErrorCode; -} - -@freezed -sealed class ServerResultI64 with _$ServerResultI64 { - const ServerResultI64._(); - - const factory ServerResultI64.ok( - PlatformInt64 field0, - ) = ServerResultI64_Ok; - const factory ServerResultI64.errorCode( - int field0, - ) = ServerResultI64_ErrorCode; -} - -@freezed -sealed class ServerResultVecU8 with _$ServerResultVecU8 { - const ServerResultVecU8._(); - - const factory ServerResultVecU8.ok( - Uint8List field0, - ) = ServerResultVecU8_Ok; - const factory ServerResultVecU8.errorCode( - int field0, - ) = ServerResultVecU8_ErrorCode; -} diff --git a/lib/core/bridge/callbacks.dart b/lib/core/bridge/callbacks.dart index 2e07f276..7c5ab5f6 100644 --- a/lib/core/bridge/callbacks.dart +++ b/lib/core/bridge/callbacks.dart @@ -4,7 +4,7 @@ // ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import import '../frb_generated.dart'; - +import '../user_config.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; // These functions are ignored because they are not marked as `pub`: `get_callbacks` @@ -28,19 +28,13 @@ Future initFlutterCallbacks({ required FutureOr> Function() legacySignalGeneratePrekeys, required FutureOr Function(PlatformInt64) apiResyncSignalSession, - required FutureOr Function(PlatformInt64) apiPushKeyRequested, - required FutureOr Function(PlatformInt64, String, String) - apiGroupMembershipError, required FutureOr Function(String, String, PlatformInt64, String) apiMediaAction, required FutureOr Function(PlatformInt64, Uint8List) apiVerificationProof, - required FutureOr Function(PlatformInt64, String?, Uint8List, int) - apiCreatePushData, required FutureOr Function(PlatformInt64) apiCreatePushAvatars, - required FutureOr Function() apiRecoveryChanged, required FutureOr Function(String, PlatformInt64) apiMediaReceived, - required FutureOr Function(String, bool) apiGroupStateRefresh, + required FutureOr Function(UserConfig) apiUserConfigChanged, }) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks( callbackId: callbackId, loggingGetStreamSink: loggingGetStreamSink, @@ -48,15 +42,11 @@ Future initFlutterCallbacks({ legacySignalEncrypt: legacySignalEncrypt, legacySignalGeneratePrekeys: legacySignalGeneratePrekeys, apiResyncSignalSession: apiResyncSignalSession, - apiPushKeyRequested: apiPushKeyRequested, - apiGroupMembershipError: apiGroupMembershipError, apiMediaAction: apiMediaAction, apiVerificationProof: apiVerificationProof, - apiCreatePushData: apiCreatePushData, apiCreatePushAvatars: apiCreatePushAvatars, - apiRecoveryChanged: apiRecoveryChanged, apiMediaReceived: apiMediaReceived, - apiGroupStateRefresh: apiGroupStateRefresh, + apiUserConfigChanged: apiUserConfigChanged, ); class LegacySignalDecryptResult { diff --git a/lib/core/bridge/groups.dart b/lib/core/bridge/groups.dart index 11b73bae..e1126d60 100644 --- a/lib/core/bridge/groups.dart +++ b/lib/core/bridge/groups.dart @@ -4,7 +4,6 @@ // 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'; Future createNewGroup({ diff --git a/lib/core/bridge/user_config.dart b/lib/core/bridge/user_config.dart new file mode 100644 index 00000000..f4889b40 --- /dev/null +++ b/lib/core/bridge/user_config.dart @@ -0,0 +1,58 @@ +// 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 '../user_config.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +class UserConfigApi { + const UserConfigApi(); + + static UserConfig clone({required UserConfig config}) => RustLib.instance.api + .crateBridgeUserConfigUserConfigApiClone(config: config); + + static Future create({ + required PlatformInt64 userId, + required String username, + required String displayName, + String? currentSetupPage, + required PlatformInt64 appVersion, + }) => RustLib.instance.api.crateBridgeUserConfigUserConfigApiCreate( + userId: userId, + username: username, + displayName: displayName, + currentSetupPage: currentSetupPage, + appVersion: appVersion, + ); + + static Future importJson({required String json}) => RustLib + .instance + .api + .crateBridgeUserConfigUserConfigApiImportJson(json: json); + + static Future load() => + RustLib.instance.api.crateBridgeUserConfigUserConfigApiLoad(); + + static Future save({required UserConfig config}) => RustLib + .instance + .api + .crateBridgeUserConfigUserConfigApiSave(config: config); + + static Future update({ + required UserConfig base, + required UserConfig config, + }) => RustLib.instance.api.crateBridgeUserConfigUserConfigApiUpdate( + base: base, + config: config, + ); + + @override + int get hashCode => 0; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserConfigApi && runtimeType == other.runtimeType; +} diff --git a/lib/core/bridge/wrapper/user_discovery.dart b/lib/core/bridge/wrapper/user_discovery.dart index bff42c1d..a3d78777 100644 --- a/lib/core/bridge/wrapper/user_discovery.dart +++ b/lib/core/bridge/wrapper/user_discovery.dart @@ -9,62 +9,25 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; class FlutterUserDiscovery { const FlutterUserDiscovery(); + static Future changeExclusionForContact({ + required int callbackId, + required PlatformInt64 contactId, + required bool exclude, + }) => RustLib.instance.api + .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContact( + callbackId: callbackId, + contactId: contactId, + exclude: exclude, + ); + + /// UI-facing read used to display the current discovery version. static Future getCurrentVersion({required int callbackId}) => RustLib.instance.api .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion( callbackId: callbackId, ); - static Future> getNewMessages({ - required int callbackId, - required PlatformInt64 contactId, - required List receivedVersion, - }) => RustLib.instance.api - .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages( - callbackId: callbackId, - contactId: contactId, - receivedVersion: receivedVersion, - ); - - static Future handleNewMessages({ - required int callbackId, - required PlatformInt64 contactId, - PlatformInt64? publicKeyVerifiedTimestamp, - required List messages, - }) => RustLib.instance.api - .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages( - callbackId: callbackId, - contactId: contactId, - publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp, - messages: messages, - ); - - static Future initializeOrUpdate({ - required int callbackId, - required int threshold, - required PlatformInt64 userId, - required List publicKey, - required bool sharePromotion, - }) => RustLib.instance.api - .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate( - callbackId: callbackId, - threshold: threshold, - userId: userId, - publicKey: publicKey, - sharePromotion: sharePromotion, - ); - - static Future shouldRequestNewMessages({ - required int callbackId, - required PlatformInt64 contactId, - required List version, - }) => RustLib.instance.api - .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages( - callbackId: callbackId, - contactId: contactId, - version: version, - ); - + /// UI-facing hook used when a user manually changes contact verification. static Future updateVerificationStateForUser({ required int callbackId, required PlatformInt64 contactId, diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 520c5e0c..e25244f1 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -8,6 +8,7 @@ import 'bridge.dart'; import 'bridge/api.dart'; import 'bridge/callbacks.dart'; import 'bridge/groups.dart'; +import 'bridge/user_config.dart'; import 'bridge/wrapper.dart'; import 'bridge/wrapper/app_database.dart'; import 'bridge/wrapper/backup.dart'; @@ -24,6 +25,7 @@ import 'keys/backup_password_keys.dart'; import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'signal/engine.dart'; +import 'user_config.dart'; /// Main entrypoint of the Rust API class RustLib extends BaseEntrypoint { @@ -82,7 +84,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => -392823520; + int get rustContentHash => -937778436; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -114,42 +116,18 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeGroupsFetchMissingGroupPublicKeys(); + Future + crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContact({ + required int callbackId, + required PlatformInt64 contactId, + required bool exclude, + }); + Future crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion({ required int callbackId, }); - Future> - crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({ - required int callbackId, - required PlatformInt64 contactId, - required List receivedVersion, - }); - - Future - crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({ - required int callbackId, - required PlatformInt64 contactId, - PlatformInt64? publicKeyVerifiedTimestamp, - required List messages, - }); - - Future - crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({ - required int callbackId, - required int threshold, - required PlatformInt64 userId, - required List publicKey, - required bool sharePromotion, - }); - - Future - crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({ - required int callbackId, - required PlatformInt64 contactId, - required List version, - }); - Future crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({ required int callbackId, @@ -174,24 +152,13 @@ abstract class RustLibApi extends BaseApi { required FutureOr> Function() legacySignalGeneratePrekeys, required FutureOr Function(PlatformInt64) apiResyncSignalSession, - required FutureOr Function(PlatformInt64) apiPushKeyRequested, - required FutureOr Function(PlatformInt64, String, String) - apiGroupMembershipError, required FutureOr Function(String, String, PlatformInt64, String) apiMediaAction, required FutureOr Function(PlatformInt64, Uint8List) apiVerificationProof, - required FutureOr Function( - PlatformInt64, - String?, - Uint8List, - int, - ) - apiCreatePushData, required FutureOr Function(PlatformInt64) apiCreatePushAvatars, - required FutureOr Function() apiRecoveryChanged, required FutureOr Function(String, PlatformInt64) apiMediaReceived, - required FutureOr Function(String, bool) apiGroupStateRefresh, + required FutureOr Function(UserConfig) apiUserConfigChanged, }); Future crateBridgeInitializeTwonlyFlutter({required InitConfig config}); @@ -215,7 +182,7 @@ abstract class RustLibApi extends BaseApi { required PlatformInt64 contactId, }); - Future crateBridgeApiRustApiAddAdditionalUser({ + Future crateBridgeApiRustApiAddAdditionalUser({ required PlatformInt64 userId, }); @@ -223,14 +190,11 @@ abstract class RustLibApi extends BaseApi { String crateBridgeApiRustApiApiBaseUrl({required String protocol}); - Future crateBridgeApiRustApiChangeUsername({ - required String username, - }); + Future crateBridgeApiRustApiChangeUsername({required String username}); Future crateBridgeApiRustApiCheckForDeletedUsernames(); - Future - crateBridgeApiRustApiCheckForPasswordlessNotification({ + Future crateBridgeApiRustApiCheckForPasswordlessNotification({ required String notificationId, required List downloadAuthToken, required Int64List alreadyReceivedMessageIds, @@ -238,7 +202,7 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeApiRustApiClose(); - Future crateBridgeApiRustApiConfirmMemoriesUpload({ + Future crateBridgeApiRustApiConfirmMemoriesUpload({ required String mediaId, }); @@ -246,35 +210,34 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeApiRustApiConnectionState(); - Future crateBridgeApiRustApiDeleteAccount(); + Future crateBridgeApiRustApiDeleteAccount(); - Future crateBridgeApiRustApiDeleteMemory({ - required String mediaId, - }); + Future crateBridgeApiRustApiDeleteMemory({required String mediaId}); - Future crateBridgeApiRustApiDisableMemoriesBackup(); + Future crateBridgeApiRustApiDisableMemoriesBackup(); - Future crateBridgeApiRustApiDownloadDone({ - required List token, - }); + Future crateBridgeApiRustApiDownloadDone({required List token}); + + Future crateBridgeApiRustApiDownloadMedia({required String mediaId}); + + Future crateBridgeApiRustApiDownloadPendingMedia(); Stream crateBridgeApiRustApiEvents(); - Future crateBridgeApiRustApiForceIpaCheck(); + Future crateBridgeApiRustApiForceIpaCheck(); - Future crateBridgeApiRustApiGetMemoriesUrl({ + Future crateBridgeApiRustApiGetMemoriesUrl({ required String mediaId, required bool thumbnail, }); - Future crateBridgeApiRustApiGetMemoriesUsage(); + Future crateBridgeApiRustApiGetMemoriesUsage(); Future crateBridgeApiRustApiGetPlanBalance(); - Future crateBridgeApiRustApiGetProofOfWork(); + Future crateBridgeApiRustApiGetProofOfWork(); - Future - crateBridgeApiRustApiGetServerKeyForPasswordlessRecovery({ + Future crateBridgeApiRustApiGetServerKeyForPasswordlessRecovery({ required PlatformInt64 userId, required List serverKeyProtection, Uint8List? pinUnlockToken, @@ -282,15 +245,15 @@ abstract class RustLibApi extends BaseApi { String? email, }); - Future crateBridgeApiRustApiGetUserById({ + Future crateBridgeApiRustApiGetUserById({ required PlatformInt64 userId, }); - Future crateBridgeApiRustApiGetUserData({ + Future crateBridgeApiRustApiGetUserData({ required String username, }); - Future crateBridgeApiRustApiGetUserIdFromUsername({ + Future crateBridgeApiRustApiGetUserIdFromUsername({ required String username, }); @@ -316,52 +279,51 @@ abstract class RustLibApi extends BaseApi { String? quoteMessageId, }); - Future crateBridgeApiRustApiIpaPurchase({ + Future crateBridgeApiRustApiIpaPurchase({ required String productId, required String source, required String verificationData, }); - Future crateBridgeApiRustApiLoadPlanBalance({ - required bool useCache, - }); + Future crateBridgeApiRustApiLoadPlanBalance(); Future crateBridgeApiRustApiNotifyMessagesOpened({ required PlatformInt64 contactId, required List messageIds, }); + Future crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat(); + Future crateBridgeApiRustApiPrepareQueuedMessage({ required String receiptId, }); - Future crateBridgeApiRustApiRegister({ + Future crateBridgeApiRustApiRegister({ required String username, required PlatformInt64 proofOfWork, required String langCode, required bool isIos, }); - Future - crateBridgeApiRustApiRegisterPasswordlessNotification({ + Future crateBridgeApiRustApiRegisterPasswordlessNotification({ required String notificationId, required List downloadAuthToken, required String langCode, String? googleFcm, }); - Future crateBridgeApiRustApiRegisterPasswordlessRecovery({ + Future crateBridgeApiRustApiRegisterPasswordlessRecovery({ required List encryptedServerKey, Uint8List? pinUnlockToken, }); Future crateBridgeApiRustApiReloadConfiguration(); - Future crateBridgeApiRustApiRemoveAdditionalUser({ + Future crateBridgeApiRustApiRemoveAdditionalUser({ required PlatformInt64 userId, }); - Future crateBridgeApiRustApiReportUser({ + Future crateBridgeApiRustApiReportUser({ required PlatformInt64 userId, required String reason, }); @@ -374,7 +336,11 @@ abstract class RustLibApi extends BaseApi { required String username, }); - Future crateBridgeApiRustApiRequestMemoriesUpload({ + Future crateBridgeApiRustApiRequestMediaReupload({ + required String mediaId, + }); + + Future crateBridgeApiRustApiRequestMemoriesUpload({ required PlatformInt64 size, required PlatformInt64 originalDate, required String mediaId, @@ -408,7 +374,7 @@ abstract class RustLibApi extends BaseApi { required String receiptId, }); - Future crateBridgeApiRustApiSendTextMessage({ + Future crateBridgeApiRustApiSendTextMessage({ required PlatformInt64 userId, required List body, Uint8List? pushData, @@ -421,30 +387,26 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeApiRustApiSetBackground({required bool inBackground}); - Future crateBridgeApiRustApiSetLoginToken({ - required List token, - }); + Future crateBridgeApiRustApiSetLoginToken({required List token}); Future crateBridgeApiRustApiSetNetworkAvailable({ required bool available, }); - Future crateBridgeApiRustApiSubmitRecoveryShare({ + Future crateBridgeApiRustApiSubmitRecoveryShare({ required String notificationId, required List encryptedMessage, }); - Future crateBridgeApiRustApiUpdateFcmToken({ - required String token, - }); + Future crateBridgeApiRustApiUpdateFcmToken({required String token}); - Future crateBridgeApiRustApiUpdateSignedPreKey({ + Future crateBridgeApiRustApiUpdateSignedPreKey({ required PlatformInt64 id, required List key, required List signature, }); - Future crateBridgeApiRustApiUploadPqcPreKeys({ + Future crateBridgeApiRustApiUploadPqcPreKeys({ required PlatformInt64 eccSignedPrekeyId, required List eccSignedPrekey, required List eccSignedPrekeySignature, @@ -605,6 +567,33 @@ abstract class RustLibApi extends BaseApi { required String groupId, required String groupName, }); + + UserConfig crateBridgeUserConfigUserConfigApiClone({ + required UserConfig config, + }); + + Future crateBridgeUserConfigUserConfigApiCreate({ + required PlatformInt64 userId, + required String username, + required String displayName, + String? currentSetupPage, + required PlatformInt64 appVersion, + }); + + Future crateBridgeUserConfigUserConfigApiImportJson({ + required String json, + }); + + Future crateBridgeUserConfigUserConfigApiLoad(); + + Future crateBridgeUserConfigUserConfigApiSave({ + required UserConfig config, + }); + + Future crateBridgeUserConfigUserConfigApiUpdate({ + required UserConfig base, + required UserConfig config, + }); } class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @@ -810,6 +799,46 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: [], ); + @override + Future + crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContact({ + required int callbackId, + required PlatformInt64 contactId, + required bool exclude, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_u_32(callbackId, serializer); + sse_encode_i_64(contactId, serializer); + sse_encode_bool(exclude, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 7, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContactConstMeta, + argValues: [callbackId, contactId, exclude], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContactConstMeta => + const TaskConstMeta( + debugName: "flutter_user_discovery_change_exclusion_for_contact", + argNames: ["callbackId", "contactId", "exclude"], + ); + @override Future crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion({ @@ -823,7 +852,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 7, + funcId: 8, port: port_, ); }, @@ -846,191 +875,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["callbackId"], ); - @override - Future> - crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages({ - required int callbackId, - required PlatformInt64 contactId, - required List receivedVersion, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_u_32(callbackId, serializer); - sse_encode_i_64(contactId, serializer); - sse_encode_list_prim_u_8_loose(receivedVersion, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 8, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_list_list_prim_u_8_strict, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: - kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta, - argValues: [callbackId, contactId, receivedVersion], - apiImpl: this, - ), - ); - } - - TaskConstMeta - get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessagesConstMeta => - const TaskConstMeta( - debugName: "flutter_user_discovery_get_new_messages", - argNames: ["callbackId", "contactId", "receivedVersion"], - ); - - @override - Future - crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages({ - required int callbackId, - required PlatformInt64 contactId, - PlatformInt64? publicKeyVerifiedTimestamp, - required List messages, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_u_32(callbackId, serializer); - sse_encode_i_64(contactId, serializer); - sse_encode_opt_box_autoadd_i_64( - publicKeyVerifiedTimestamp, - serializer, - ); - sse_encode_list_list_prim_u_8_strict(messages, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 9, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: - kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta, - argValues: [ - callbackId, - contactId, - publicKeyVerifiedTimestamp, - messages, - ], - apiImpl: this, - ), - ); - } - - TaskConstMeta - get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessagesConstMeta => - const TaskConstMeta( - debugName: "flutter_user_discovery_handle_new_messages", - argNames: [ - "callbackId", - "contactId", - "publicKeyVerifiedTimestamp", - "messages", - ], - ); - - @override - Future - crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate({ - required int callbackId, - required int threshold, - required PlatformInt64 userId, - required List publicKey, - required bool sharePromotion, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_u_32(callbackId, serializer); - sse_encode_u_8(threshold, serializer); - sse_encode_i_64(userId, serializer); - sse_encode_list_prim_u_8_loose(publicKey, serializer); - sse_encode_bool(sharePromotion, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 10, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: - kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta, - argValues: [callbackId, threshold, userId, publicKey, sharePromotion], - apiImpl: this, - ), - ); - } - - TaskConstMeta - get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdateConstMeta => - const TaskConstMeta( - debugName: "flutter_user_discovery_initialize_or_update", - argNames: [ - "callbackId", - "threshold", - "userId", - "publicKey", - "sharePromotion", - ], - ); - - @override - Future - crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages({ - required int callbackId, - required PlatformInt64 contactId, - required List version, - }) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_u_32(callbackId, serializer); - sse_encode_i_64(contactId, serializer); - sse_encode_list_prim_u_8_loose(version, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 11, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_opt_list_prim_u_8_strict, - decodeErrorData: sse_decode_AnyhowException, - ), - constMeta: - kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta, - argValues: [callbackId, contactId, version], - apiImpl: this, - ), - ); - } - - TaskConstMeta - get kCrateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessagesConstMeta => - const TaskConstMeta( - debugName: "flutter_user_discovery_should_request_new_messages", - argNames: ["callbackId", "contactId", "version"], - ); - @override Future crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryUpdateVerificationStateForUser({ @@ -1051,7 +895,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 12, + funcId: 9, port: port_, ); }, @@ -1092,24 +936,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required FutureOr> Function() legacySignalGeneratePrekeys, required FutureOr Function(PlatformInt64) apiResyncSignalSession, - required FutureOr Function(PlatformInt64) apiPushKeyRequested, - required FutureOr Function(PlatformInt64, String, String) - apiGroupMembershipError, required FutureOr Function(String, String, PlatformInt64, String) apiMediaAction, required FutureOr Function(PlatformInt64, Uint8List) apiVerificationProof, - required FutureOr Function( - PlatformInt64, - String?, - Uint8List, - int, - ) - apiCreatePushData, required FutureOr Function(PlatformInt64) apiCreatePushAvatars, - required FutureOr Function() apiRecoveryChanged, required FutureOr Function(String, PlatformInt64) apiMediaReceived, - required FutureOr Function(String, bool) apiGroupStateRefresh, + required FutureOr Function(UserConfig) apiUserConfigChanged, }) { return handler.executeNormal( NormalTask( @@ -1136,14 +969,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { apiResyncSignalSession, serializer, ); - sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( - apiPushKeyRequested, - serializer, - ); - sse_encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException( - apiGroupMembershipError, - serializer, - ); sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( apiMediaAction, serializer, @@ -1152,30 +977,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { apiVerificationProof, serializer, ); - sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( - apiCreatePushData, - serializer, - ); sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( apiCreatePushAvatars, serializer, ); - sse_encode_DartFn_Inputs__Output_unit_AnyhowException( - apiRecoveryChanged, - serializer, - ); sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException( apiMediaReceived, serializer, ); - sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException( - apiGroupStateRefresh, + sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException( + apiUserConfigChanged, serializer, ); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 13, + funcId: 10, port: port_, ); }, @@ -1191,15 +1008,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { legacySignalEncrypt, legacySignalGeneratePrekeys, apiResyncSignalSession, - apiPushKeyRequested, - apiGroupMembershipError, apiMediaAction, apiVerificationProof, - apiCreatePushData, apiCreatePushAvatars, - apiRecoveryChanged, apiMediaReceived, - apiGroupStateRefresh, + apiUserConfigChanged, ], apiImpl: this, ), @@ -1216,15 +1029,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { "legacySignalEncrypt", "legacySignalGeneratePrekeys", "apiResyncSignalSession", - "apiPushKeyRequested", - "apiGroupMembershipError", "apiMediaAction", "apiVerificationProof", - "apiCreatePushData", "apiCreatePushAvatars", - "apiRecoveryChanged", "apiMediaReceived", - "apiGroupStateRefresh", + "apiUserConfigChanged", ], ); @@ -1240,7 +1049,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 14, + funcId: 11, port: port_, ); }, @@ -1273,7 +1082,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 15, + funcId: 12, port: port_, ); }, @@ -1304,7 +1113,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 16, + funcId: 13, port: port_, ); }, @@ -1343,7 +1152,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 17, + funcId: 14, port: port_, ); }, @@ -1380,7 +1189,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 18, + funcId: 15, port: port_, ); }, @@ -1402,7 +1211,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiAddAdditionalUser({ + Future crateBridgeApiRustApiAddAdditionalUser({ required PlatformInt64 userId, }) { return handler.executeNormal( @@ -1413,12 +1222,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 19, + funcId: 16, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiAddAdditionalUserConstMeta, @@ -1443,7 +1252,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 17, port: port_, ); }, @@ -1471,7 +1280,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(protocol, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1491,9 +1300,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiChangeUsername({ - required String username, - }) { + Future crateBridgeApiRustApiChangeUsername({required String username}) { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -1502,12 +1309,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 22, + funcId: 19, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiChangeUsernameConstMeta, @@ -1532,7 +1339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 20, port: port_, ); }, @@ -1554,8 +1361,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future - crateBridgeApiRustApiCheckForPasswordlessNotification({ + Future crateBridgeApiRustApiCheckForPasswordlessNotification({ required String notificationId, required List downloadAuthToken, required Int64List alreadyReceivedMessageIds, @@ -1573,12 +1379,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 24, + funcId: 21, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_vec_u_8, + decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: @@ -1613,7 +1419,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 25, + funcId: 22, port: port_, ); }, @@ -1634,7 +1440,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiConfirmMemoriesUpload({ + Future crateBridgeApiRustApiConfirmMemoriesUpload({ required String mediaId, }) { return handler.executeNormal( @@ -1645,12 +1451,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 26, + funcId: 23, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiConfirmMemoriesUploadConstMeta, @@ -1675,7 +1481,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 27, + funcId: 24, port: port_, ); }, @@ -1705,7 +1511,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 28, + funcId: 25, port: port_, ); }, @@ -1727,7 +1533,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiDeleteAccount() { + Future crateBridgeApiRustApiDeleteAccount() { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -1735,12 +1541,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 29, + funcId: 26, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiDeleteAccountConstMeta, @@ -1757,9 +1563,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiDeleteMemory({ - required String mediaId, - }) { + Future crateBridgeApiRustApiDeleteMemory({required String mediaId}) { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -1768,12 +1572,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 30, + funcId: 27, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiDeleteMemoryConstMeta, @@ -1790,7 +1594,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiDisableMemoriesBackup() { + Future crateBridgeApiRustApiDisableMemoriesBackup() { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -1798,12 +1602,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 31, + funcId: 28, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiDisableMemoriesBackupConstMeta, @@ -1820,9 +1624,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiDownloadDone({ - required List token, - }) { + Future crateBridgeApiRustApiDownloadDone({required List token}) { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -1831,12 +1633,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 32, + funcId: 29, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiDownloadDoneConstMeta, @@ -1852,6 +1654,67 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["token"], ); + @override + Future crateBridgeApiRustApiDownloadMedia({required String mediaId}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(mediaId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 30, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeApiRustApiDownloadMediaConstMeta, + argValues: [mediaId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeApiRustApiDownloadMediaConstMeta => + const TaskConstMeta( + debugName: "rust_api_download_media", + argNames: ["mediaId"], + ); + + @override + Future crateBridgeApiRustApiDownloadPendingMedia() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 31, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeApiRustApiDownloadPendingMediaConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeApiRustApiDownloadPendingMediaConstMeta => + const TaskConstMeta( + debugName: "rust_api_download_pending_media", + argNames: [], + ); + @override Stream crateBridgeApiRustApiEvents() { final sink = RustStreamSink(); @@ -1864,7 +1727,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 33, + funcId: 32, port: port_, ); }, @@ -1888,7 +1751,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiForceIpaCheck() { + Future crateBridgeApiRustApiForceIpaCheck() { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -1896,12 +1759,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 34, + funcId: 33, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiForceIpaCheckConstMeta, @@ -1918,7 +1781,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiGetMemoriesUrl({ + Future crateBridgeApiRustApiGetMemoriesUrl({ required String mediaId, required bool thumbnail, }) { @@ -1931,12 +1794,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 35, + funcId: 34, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_vec_u_8, + decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiGetMemoriesUrlConstMeta, @@ -1953,7 +1816,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiGetMemoriesUsage() { + Future crateBridgeApiRustApiGetMemoriesUsage() { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -1961,12 +1824,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 36, + funcId: 35, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_vec_u_8, + decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiGetMemoriesUsageConstMeta, @@ -1991,7 +1854,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 37, + funcId: 36, port: port_, ); }, @@ -2013,7 +1876,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiGetProofOfWork() { + Future crateBridgeApiRustApiGetProofOfWork() { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -2021,12 +1884,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 38, + funcId: 37, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_vec_u_8, + decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiGetProofOfWorkConstMeta, @@ -2043,8 +1906,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future - crateBridgeApiRustApiGetServerKeyForPasswordlessRecovery({ + Future crateBridgeApiRustApiGetServerKeyForPasswordlessRecovery({ required PlatformInt64 userId, required List serverKeyProtection, Uint8List? pinUnlockToken, @@ -2063,12 +1925,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 39, + funcId: 38, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_vec_u_8, + decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: @@ -2099,7 +1961,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiGetUserById({ + Future crateBridgeApiRustApiGetUserById({ required PlatformInt64 userId, }) { return handler.executeNormal( @@ -2110,12 +1972,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 40, + funcId: 39, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_vec_u_8, + decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiGetUserByIdConstMeta, @@ -2132,7 +1994,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiGetUserData({ + Future crateBridgeApiRustApiGetUserData({ required String username, }) { return handler.executeNormal( @@ -2143,12 +2005,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 41, + funcId: 40, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_vec_u_8, + decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiGetUserDataConstMeta, @@ -2165,7 +2027,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiGetUserIdFromUsername({ + Future crateBridgeApiRustApiGetUserIdFromUsername({ required String username, }) { return handler.executeNormal( @@ -2176,12 +2038,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 42, + funcId: 41, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_i_64, + decodeSuccessData: sse_decode_i_64, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiGetUserIdFromUsernameConstMeta, @@ -2213,7 +2075,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 42, port: port_, ); }, @@ -2249,7 +2111,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 44, + funcId: 43, port: port_, ); }, @@ -2284,7 +2146,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 45, + funcId: 44, port: port_, ); }, @@ -2321,7 +2183,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 45, port: port_, ); }, @@ -2343,7 +2205,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiIpaPurchase({ + Future crateBridgeApiRustApiIpaPurchase({ required String productId, required String source, required String verificationData, @@ -2358,12 +2220,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 47, + funcId: 46, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiIpaPurchaseConstMeta, @@ -2380,18 +2242,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiLoadPlanBalance({ - required bool useCache, - }) { + Future crateBridgeApiRustApiLoadPlanBalance() { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_bool(useCache, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 48, + funcId: 47, port: port_, ); }, @@ -2400,7 +2259,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiLoadPlanBalanceConstMeta, - argValues: [useCache], + argValues: [], apiImpl: this, ), ); @@ -2409,7 +2268,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateBridgeApiRustApiLoadPlanBalanceConstMeta => const TaskConstMeta( debugName: "rust_api_load_plan_balance", - argNames: ["useCache"], + argNames: [], ); @override @@ -2426,7 +2285,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 49, + funcId: 48, port: port_, ); }, @@ -2447,6 +2306,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["contactId", "messageIds"], ); + @override + Future crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 49, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeatConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeatConstMeta => + const TaskConstMeta( + debugName: "rust_api_perform_passwordless_recovery_heartbeat", + argNames: [], + ); + @override Future crateBridgeApiRustApiPrepareQueuedMessage({ required String receiptId, @@ -2482,7 +2373,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiRegister({ + Future crateBridgeApiRustApiRegister({ required String username, required PlatformInt64 proofOfWork, required String langCode, @@ -2504,7 +2395,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_i_64, + decodeSuccessData: sse_decode_i_64, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiRegisterConstMeta, @@ -2521,8 +2412,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future - crateBridgeApiRustApiRegisterPasswordlessNotification({ + Future crateBridgeApiRustApiRegisterPasswordlessNotification({ required String notificationId, required List downloadAuthToken, required String langCode, @@ -2544,7 +2434,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: @@ -2568,7 +2458,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiRegisterPasswordlessRecovery({ + Future crateBridgeApiRustApiRegisterPasswordlessRecovery({ required List encryptedServerKey, Uint8List? pinUnlockToken, }) { @@ -2586,7 +2476,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiRegisterPasswordlessRecoveryConstMeta, @@ -2634,7 +2524,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiRemoveAdditionalUser({ + Future crateBridgeApiRustApiRemoveAdditionalUser({ required PlatformInt64 userId, }) { return handler.executeNormal( @@ -2650,7 +2540,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiRemoveAdditionalUserConstMeta, @@ -2667,7 +2557,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiReportUser({ + Future crateBridgeApiRustApiReportUser({ required PlatformInt64 userId, required String reason, }) { @@ -2685,7 +2575,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiReportUserConstMeta, @@ -2768,7 +2658,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiRequestMemoriesUpload({ + Future crateBridgeApiRustApiRequestMediaReupload({ + required String mediaId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(mediaId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 59, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeApiRustApiRequestMediaReuploadConstMeta, + argValues: [mediaId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeApiRustApiRequestMediaReuploadConstMeta => + const TaskConstMeta( + debugName: "rust_api_request_media_reupload", + argNames: ["mediaId"], + ); + + @override + Future crateBridgeApiRustApiRequestMemoriesUpload({ required PlatformInt64 size, required PlatformInt64 originalDate, required String mediaId, @@ -2783,12 +2706,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 59, + funcId: 60, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_vec_u_8, + decodeSuccessData: sse_decode_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiRequestMemoriesUploadConstMeta, @@ -2813,7 +2736,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 60, + funcId: 61, port: port_, ); }, @@ -2844,7 +2767,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 61, + funcId: 62, port: port_, ); }, @@ -2877,7 +2800,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 62, + funcId: 63, port: port_, ); }, @@ -2920,7 +2843,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 63, + funcId: 64, port: port_, ); }, @@ -2974,7 +2897,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 64, + funcId: 65, port: port_, ); }, @@ -3013,7 +2936,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 65, + funcId: 66, port: port_, ); }, @@ -3035,7 +2958,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiSendTextMessage({ + Future crateBridgeApiRustApiSendTextMessage({ required PlatformInt64 userId, required List body, Uint8List? pushData, @@ -3050,12 +2973,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 66, + funcId: 67, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiSendTextMessageConstMeta, @@ -3085,7 +3008,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 67, + funcId: 68, port: port_, ); }, @@ -3118,7 +3041,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 68, + funcId: 69, port: port_, ); }, @@ -3140,9 +3063,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiSetLoginToken({ - required List token, - }) { + Future crateBridgeApiRustApiSetLoginToken({required List token}) { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -3151,12 +3072,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 69, + funcId: 70, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiSetLoginTokenConstMeta, @@ -3184,7 +3105,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 70, + funcId: 71, port: port_, ); }, @@ -3206,7 +3127,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiSubmitRecoveryShare({ + Future crateBridgeApiRustApiSubmitRecoveryShare({ required String notificationId, required List encryptedMessage, }) { @@ -3219,12 +3140,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 71, + funcId: 72, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiSubmitRecoveryShareConstMeta, @@ -3241,9 +3162,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiUpdateFcmToken({ - required String token, - }) { + Future crateBridgeApiRustApiUpdateFcmToken({required String token}) { return handler.executeNormal( NormalTask( callFfi: (port_) { @@ -3252,12 +3171,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 72, + funcId: 73, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_server_result_empty, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiUpdateFcmTokenConstMeta, @@ -3274,7 +3193,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiUpdateSignedPreKey({ + Future crateBridgeApiRustApiUpdateSignedPreKey({ required PlatformInt64 id, required List key, required List signature, @@ -3289,12 +3208,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 73, + funcId: 74, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiUpdateSignedPreKeyConstMeta, @@ -3311,7 +3230,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiUploadPqcPreKeys({ + Future crateBridgeApiRustApiUploadPqcPreKeys({ required PlatformInt64 eccSignedPrekeyId, required List eccSignedPrekey, required List eccSignedPrekeySignature, @@ -3337,12 +3256,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 74, + funcId: 75, port: port_, ); }, codec: SseCodec( - decodeSuccessData: sse_decode_list_prim_u_8_strict, + decodeSuccessData: sse_decode_unit, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiUploadPqcPreKeysConstMeta, @@ -3389,7 +3308,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 75, + funcId: 76, port: port_, ); }, @@ -3422,7 +3341,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 76, + funcId: 77, port: port_, ); }, @@ -3455,7 +3374,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 78, port: port_, ); }, @@ -3492,7 +3411,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 78, + funcId: 79, port: port_, ); }, @@ -3524,7 +3443,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 80, port: port_, ); }, @@ -3557,7 +3476,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 81, port: port_, ); }, @@ -3592,7 +3511,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 82, port: port_, ); }, @@ -3624,7 +3543,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 83, port: port_, ); }, @@ -3662,7 +3581,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 83, + funcId: 84, port: port_, ); }, @@ -3695,7 +3614,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 85, port: port_, ); }, @@ -3733,7 +3652,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 86, port: port_, ); }, @@ -3770,7 +3689,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 87, port: port_, ); }, @@ -3807,7 +3726,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 88, port: port_, ); }, @@ -3845,7 +3764,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 89, port: port_, ); }, @@ -3883,7 +3802,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 89, + funcId: 90, port: port_, ); }, @@ -3915,7 +3834,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 90, + funcId: 91, port: port_, ); }, @@ -3948,7 +3867,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 91, + funcId: 92, port: port_, ); }, @@ -3980,7 +3899,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 92, + funcId: 93, port: port_, ); }, @@ -4015,7 +3934,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 93, + funcId: 94, port: port_, ); }, @@ -4057,7 +3976,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 94, + funcId: 95, port: port_, ); }, @@ -4101,7 +4020,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 95, + funcId: 96, port: port_, ); }, @@ -4134,7 +4053,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 96, + funcId: 97, port: port_, ); }, @@ -4166,7 +4085,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 97, + funcId: 98, port: port_, ); }, @@ -4201,7 +4120,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 98, + funcId: 99, port: port_, ); }, @@ -4233,7 +4152,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 99, + funcId: 100, port: port_, ); }, @@ -4268,7 +4187,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 101, port: port_, ); }, @@ -4305,7 +4224,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 102, port: port_, ); }, @@ -4344,7 +4263,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 103, port: port_, ); }, @@ -4381,7 +4300,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 104, port: port_, ); }, @@ -4411,7 +4330,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 105, port: port_, ); }, @@ -4443,7 +4362,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 106, port: port_, ); }, @@ -4482,7 +4401,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 107, port: port_, ); }, @@ -4521,7 +4440,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 108, port: port_, ); }, @@ -4556,7 +4475,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 109, port: port_, ); }, @@ -4591,7 +4510,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 110, port: port_, ); }, @@ -4626,7 +4545,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 111, port: port_, ); }, @@ -4647,6 +4566,222 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["groupId", "groupName"], ); + @override + UserConfig crateBridgeUserConfigUserConfigApiClone({ + required UserConfig config, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_user_config(config, serializer); + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 112, + )!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_user_config, + decodeErrorData: null, + ), + constMeta: kCrateBridgeUserConfigUserConfigApiCloneConstMeta, + argValues: [config], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeUserConfigUserConfigApiCloneConstMeta => + const TaskConstMeta( + debugName: "user_config_api_clone", + argNames: ["config"], + ); + + @override + Future crateBridgeUserConfigUserConfigApiCreate({ + required PlatformInt64 userId, + required String username, + required String displayName, + String? currentSetupPage, + required PlatformInt64 appVersion, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(userId, serializer); + sse_encode_String(username, serializer); + sse_encode_String(displayName, serializer); + sse_encode_opt_String(currentSetupPage, serializer); + sse_encode_i_64(appVersion, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 113, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_user_config, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeUserConfigUserConfigApiCreateConstMeta, + argValues: [ + userId, + username, + displayName, + currentSetupPage, + appVersion, + ], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeUserConfigUserConfigApiCreateConstMeta => + const TaskConstMeta( + debugName: "user_config_api_create", + argNames: [ + "userId", + "username", + "displayName", + "currentSetupPage", + "appVersion", + ], + ); + + @override + Future crateBridgeUserConfigUserConfigApiImportJson({ + required String json, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(json, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 114, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_user_config, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeUserConfigUserConfigApiImportJsonConstMeta, + argValues: [json], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeUserConfigUserConfigApiImportJsonConstMeta => + const TaskConstMeta( + debugName: "user_config_api_import_json", + argNames: ["json"], + ); + + @override + Future crateBridgeUserConfigUserConfigApiLoad() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 115, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_box_autoadd_user_config, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeUserConfigUserConfigApiLoadConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeUserConfigUserConfigApiLoadConstMeta => + const TaskConstMeta( + debugName: "user_config_api_load", + argNames: [], + ); + + @override + Future crateBridgeUserConfigUserConfigApiSave({ + required UserConfig config, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_user_config(config, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 116, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_user_config, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeUserConfigUserConfigApiSaveConstMeta, + argValues: [config], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeUserConfigUserConfigApiSaveConstMeta => + const TaskConstMeta( + debugName: "user_config_api_save", + argNames: ["config"], + ); + + @override + Future crateBridgeUserConfigUserConfigApiUpdate({ + required UserConfig base, + required UserConfig config, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_box_autoadd_user_config(base, serializer); + sse_encode_box_autoadd_user_config(config, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 117, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_user_config, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeUserConfigUserConfigApiUpdateConstMeta, + argValues: [base, config], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeUserConfigUserConfigApiUpdateConstMeta => + const TaskConstMeta( + debugName: "user_config_api_update", + argNames: ["base", "config"], + ); + Future Function(int, dynamic, dynamic, dynamic, dynamic) encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( FutureOr Function(String, String, PlatformInt64, String) raw, @@ -4685,42 +4820,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { }; } - Future Function(int, dynamic, dynamic) - encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException( - FutureOr Function(String, bool) raw, - ) { - return (callId, rawArg0, rawArg1) async { - final arg0 = dco_decode_String(rawArg0); - final arg1 = dco_decode_bool(rawArg1); - - Box? rawOutput; - Box? rawError; - try { - rawOutput = Box(await raw(arg0, arg1)); - } catch (e, s) { - rawError = Box(AnyhowException("$e\n\n$s")); - } - - final serializer = SseSerializer(generalizedFrbRustBinding); - assert((rawOutput != null) ^ (rawError != null)); - if (rawOutput != null) { - serializer.buffer.putUint8(0); - sse_encode_unit(rawOutput.value, serializer); - } else { - serializer.buffer.putUint8(1); - sse_encode_AnyhowException(rawError!.value, serializer); - } - final output = serializer.intoRaw(); - - generalizedFrbRustBinding.dartFnDeliverOutput( - callId: callId, - ptr: output.ptr, - rustVecLen: output.rustVecLen, - dataLen: output.dataLen, - ); - }; - } - Future Function(int, dynamic, dynamic) encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException( FutureOr Function(String, PlatformInt64) raw, @@ -4831,43 +4930,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { }; } - Future Function( - int, - ) - encode_DartFn_Inputs__Output_unit_AnyhowException( - FutureOr Function() raw, - ) { - return ( - callId, - ) async { - Box? rawOutput; - Box? rawError; - try { - rawOutput = Box(await raw()); - } catch (e, s) { - rawError = Box(AnyhowException("$e\n\n$s")); - } - - final serializer = SseSerializer(generalizedFrbRustBinding); - assert((rawOutput != null) ^ (rawError != null)); - if (rawOutput != null) { - serializer.buffer.putUint8(0); - sse_encode_unit(rawOutput.value, serializer); - } else { - serializer.buffer.putUint8(1); - sse_encode_AnyhowException(rawError!.value, serializer); - } - final output = serializer.intoRaw(); - - generalizedFrbRustBinding.dartFnDeliverOutput( - callId: callId, - ptr: output.ptr, - rustVecLen: output.rustVecLen, - dataLen: output.dataLen, - ); - }; - } - Future Function(int, dynamic) encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( FutureOr Function(PlatformInt64) raw, @@ -4903,43 +4965,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { }; } - Future Function(int, dynamic, dynamic, dynamic) - encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException( - FutureOr Function(PlatformInt64, String, String) raw, - ) { - return (callId, rawArg0, rawArg1, rawArg2) async { - final arg0 = dco_decode_i_64(rawArg0); - final arg1 = dco_decode_String(rawArg1); - final arg2 = dco_decode_String(rawArg2); - - Box? rawOutput; - Box? rawError; - try { - rawOutput = Box(await raw(arg0, arg1, arg2)); - } catch (e, s) { - rawError = Box(AnyhowException("$e\n\n$s")); - } - - final serializer = SseSerializer(generalizedFrbRustBinding); - assert((rawOutput != null) ^ (rawError != null)); - if (rawOutput != null) { - serializer.buffer.putUint8(0); - sse_encode_unit(rawOutput.value, serializer); - } else { - serializer.buffer.putUint8(1); - sse_encode_AnyhowException(rawError!.value, serializer); - } - final output = serializer.intoRaw(); - - generalizedFrbRustBinding.dartFnDeliverOutput( - callId: callId, - ptr: output.ptr, - rustVecLen: output.rustVecLen, - dataLen: output.dataLen, - ); - }; - } - Future Function(int, dynamic, dynamic) encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( FutureOr Function(PlatformInt64, Uint8List) raw, @@ -5053,20 +5078,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { }; } - Future Function(int, dynamic, dynamic, dynamic, dynamic) - encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( - FutureOr Function(PlatformInt64, String?, Uint8List, int) raw, + Future Function(int, dynamic) + encode_DartFn_Inputs_user_config_Output_unit_AnyhowException( + FutureOr Function(UserConfig) raw, ) { - return (callId, rawArg0, rawArg1, rawArg2, rawArg3) async { - final arg0 = dco_decode_i_64(rawArg0); - final arg1 = dco_decode_opt_String(rawArg1); - final arg2 = dco_decode_list_prim_u_8_strict(rawArg2); - final arg3 = dco_decode_i_32(rawArg3); + return (callId, rawArg0) async { + final arg0 = dco_decode_user_config(rawArg0); - Box? rawOutput; + Box? rawOutput; Box? rawError; try { - rawOutput = Box(await raw(arg0, arg1, arg2, arg3)); + rawOutput = Box(await raw(arg0)); } catch (e, s) { rawError = Box(AnyhowException("$e\n\n$s")); } @@ -5075,7 +5097,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { assert((rawOutput != null) ^ (rawError != null)); if (rawOutput != null) { serializer.buffer.putUint8(0); - sse_encode_opt_list_prim_u_8_strict(rawOutput.value, serializer); + sse_encode_unit(rawOutput.value, serializer); } else { serializer.buffer.putUint8(1); sse_encode_AnyhowException(rawError!.value, serializer); @@ -5098,17 +5120,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - FutureOr Function(String, String, PlatformInt64, String) - dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( - dynamic raw, - ) { + DateTime dco_decode_Chrono_Utc(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - throw UnimplementedError(''); + return dcoDecodeTimestamp(ts: dco_decode_i_64(raw).toInt(), isUtc: true); } @protected - FutureOr Function(String, bool) - dco_decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException( + FutureOr Function(String, String, PlatformInt64, String) + dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( dynamic raw, ) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5142,13 +5161,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { throw UnimplementedError(''); } - @protected - FutureOr Function() - dco_decode_DartFn_Inputs__Output_unit_AnyhowException(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - throw UnimplementedError(''); - } - @protected FutureOr Function(PlatformInt64) dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw) { @@ -5156,15 +5168,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { throw UnimplementedError(''); } - @protected - FutureOr Function(PlatformInt64, String, String) - dco_decode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - throw UnimplementedError(''); - } - @protected FutureOr Function(PlatformInt64, Uint8List) dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( @@ -5193,8 +5196,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - FutureOr Function(PlatformInt64, String?, Uint8List, int) - dco_decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( + FutureOr Function(UserConfig) + dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException( dynamic raw, ) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5207,6 +5210,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return decodeDartOpaque(raw, generalizedFrbRustBinding); } + @protected + Map> dco_decode_Map_String_list_String_None( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return Map.fromEntries( + dco_decode_list_record_string_list_string( + raw, + ).map((e) => MapEntry(e.$1, e.$2)), + ); + } + @protected Map dco_decode_Map_i_64_list_prim_u_8_strict_None( dynamic raw, @@ -5280,6 +5295,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as bool; } + @protected + DateTime dco_decode_box_autoadd_Chrono_Utc(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_Chrono_Utc(raw); + } + @protected ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5330,6 +5351,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_legacy_signal_encrypt_result(raw); } + @protected + PasswordlessRecoveryConfig + dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_passwordless_recovery_config(raw); + } + @protected PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message( dynamic raw, @@ -5338,12 +5366,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_prepared_outgoing_message(raw); } + @protected + TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_twonly_safe_backup(raw); + } + @protected int dco_decode_box_autoadd_u_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw as int; } + @protected + UserConfig dco_decode_box_autoadd_user_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_user_config(raw); + } + @protected double dco_decode_f_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5425,6 +5465,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dcoDecodeI64(raw); } + @protected + LastBackupUploadState dco_decode_last_backup_upload_state(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return LastBackupUploadState.values[raw as int]; + } + @protected LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5560,6 +5606,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { .toList(); } + @protected + List<(String, List)> dco_decode_list_record_string_list_string( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List) + .map(dco_decode_record_string_list_string) + .toList(); + } + @protected List dco_decode_list_sql_row(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5572,12 +5628,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (raw as List).map(dco_decode_sql_value).toList(); } + @protected + Map>? dco_decode_opt_Map_String_list_String_None( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_Map_String_list_String_None(raw); + } + @protected String? dco_decode_opt_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw == null ? null : dco_decode_String(raw); } + @protected + DateTime? dco_decode_opt_box_autoadd_Chrono_Utc(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_Chrono_Utc(raw); + } + @protected ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state( dynamic raw, @@ -5615,6 +5685,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { : dco_decode_box_autoadd_legacy_signal_encrypt_result(raw); } + @protected + PasswordlessRecoveryConfig? + dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null + ? null + : dco_decode_box_autoadd_passwordless_recovery_config(raw); + } + @protected PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message( dynamic raw, @@ -5625,18 +5704,55 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { : dco_decode_box_autoadd_prepared_outgoing_message(raw); } + @protected + TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_twonly_safe_backup(raw); + } + @protected int? dco_decode_opt_box_autoadd_u_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw == null ? null : dco_decode_box_autoadd_u_32(raw); } + @protected + UserConfig? dco_decode_opt_box_autoadd_user_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_user_config(raw); + } + + @protected + List? dco_decode_opt_list_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_list_String(raw); + } + @protected Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw == null ? null : dco_decode_list_prim_u_8_strict(raw); } + @protected + PasswordlessRecoveryConfig dco_decode_passwordless_recovery_config( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 7) + throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); + return PasswordlessRecoveryConfig( + email: dco_decode_opt_String(arr[0]), + threshold: dco_decode_i_64(arr[1]), + serverKeyProtection: dco_decode_opt_list_prim_u_8_strict(arr[2]), + pinUnlockToken: dco_decode_opt_list_prim_u_8_strict(arr[3]), + lastServerHeartbeat: dco_decode_opt_box_autoadd_Chrono_Utc(arr[4]), + lastContactHeartbeat: dco_decode_opt_box_autoadd_Chrono_Utc(arr[5]), + encryptedServerKey: dco_decode_opt_list_prim_u_8_strict(arr[6]), + ); + } + @protected PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5694,6 +5810,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + (String, List) dco_decode_record_string_list_string(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return ( + dco_decode_String(arr[0]), + dco_decode_list_String(arr[1]), + ); + } + @protected (String, String) dco_decode_record_string_string(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5771,52 +5900,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - ServerResultEmpty dco_decode_server_result_empty(dynamic raw) { + SetupProfile dco_decode_setup_profile(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs - switch (raw[0]) { - case 0: - return ServerResultEmpty_Ok(); - case 1: - return ServerResultEmpty_ErrorCode( - dco_decode_i_32(raw[1]), - ); - default: - throw Exception("unreachable"); - } - } - - @protected - ServerResultI64 dco_decode_server_result_i_64(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - switch (raw[0]) { - case 0: - return ServerResultI64_Ok( - dco_decode_i_64(raw[1]), - ); - case 1: - return ServerResultI64_ErrorCode( - dco_decode_i_32(raw[1]), - ); - default: - throw Exception("unreachable"); - } - } - - @protected - ServerResultVecU8 dco_decode_server_result_vec_u_8(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - switch (raw[0]) { - case 0: - return ServerResultVecU8_Ok( - dco_decode_list_prim_u_8_strict(raw[1]), - ); - case 1: - return ServerResultVecU8_ErrorCode( - dco_decode_i_32(raw[1]), - ); - default: - throw Exception("unreachable"); - } + return SetupProfile.values[raw as int]; } @protected @@ -5869,6 +5955,27 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + ThemeMode dco_decode_theme_mode(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return ThemeMode.values[raw as int]; + } + + @protected + TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return TwonlySafeBackup( + lastBackupSize: dco_decode_i_64(arr[0]), + backupUploadState: dco_decode_last_backup_upload_state(arr[1]), + lastBackupDone: dco_decode_opt_box_autoadd_Chrono_Utc(arr[2]), + backupId: dco_decode_list_prim_u_8_strict(arr[3]), + encryptionKey: dco_decode_list_prim_u_8_strict(arr[4]), + ); + } + @protected int dco_decode_u_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5899,6 +6006,88 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return; } + @protected + UserConfig dco_decode_user_config(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 58) + throw Exception('unexpected arr length: expect 58 but see ${arr.length}'); + return UserConfig( + userId: dco_decode_i_64(arr[0]), + username: dco_decode_String(arr[1]), + displayName: dco_decode_String(arr[2]), + avatarSvg: dco_decode_opt_String(arr[3]), + appVersion: dco_decode_i_64(arr[4]), + avatarCounter: dco_decode_i_64(arr[5]), + videoStabilizationEnabled: dco_decode_bool(arr[6]), + isDeveloper: dco_decode_bool(arr[7]), + deviceId: dco_decode_i_64(arr[8]), + setupProfile: dco_decode_setup_profile(arr[9]), + subscriptionPlan: dco_decode_String(arr[10]), + subscriptionPlanIdStore: dco_decode_opt_String(arr[11]), + lastImageSend: dco_decode_opt_box_autoadd_Chrono_Utc(arr[12]), + todaysImageCounter: dco_decode_opt_box_autoadd_i_64(arr[13]), + lastPlanBallance: dco_decode_opt_String(arr[14]), + additionalUserInvites: dco_decode_opt_String(arr[15]), + themeMode: dco_decode_theme_mode(arr[16]), + primaryColorValue: dco_decode_opt_box_autoadd_i_64(arr[17]), + defaultShowTime: dco_decode_opt_box_autoadd_i_64(arr[18]), + requestedAudioPermission: dco_decode_bool(arr[19]), + enableDatabaseLogging: dco_decode_bool(arr[20]), + automaticallyMarkEqualMediaFilesAsOpened: dco_decode_bool(arr[21]), + showNewsShortcut: dco_decode_bool(arr[22]), + showShowImagePreviewWhenSending: dco_decode_bool(arr[23]), + startWithCameraOpen: dco_decode_bool(arr[24]), + preSelectedEmojies: dco_decode_opt_list_String(arr[25]), + autoDownloadOptions: dco_decode_opt_Map_String_list_String_None(arr[26]), + storeMediaFilesInGallery: dco_decode_bool(arr[27]), + autoStoreAllSendUnlimitedMediaFiles: dco_decode_bool(arr[28]), + typingIndicators: dco_decode_bool(arr[29]), + showRestoreFlame: dco_decode_bool(arr[30]), + myBestFriendGroupId: dco_decode_opt_String(arr[31]), + signalLastSignedPreKeyUpdated: dco_decode_opt_box_autoadd_Chrono_Utc( + arr[32], + ), + signalLastPqcPreKeysUploaded: dco_decode_opt_box_autoadd_Chrono_Utc( + arr[33], + ), + allowErrorTrackingViaSentry: dco_decode_bool(arr[34]), + screenLockEnabled: dco_decode_bool(arr[35]), + isCloudBackupEnabled: dco_decode_bool(arr[36]), + isUserDiscoveryEnabled: dco_decode_bool(arr[37]), + requiredSendImages: dco_decode_i_64(arr[38]), + userDiscoveryThreshold: dco_decode_u_8(arr[39]), + userDiscoveryRequiresManualApproval: dco_decode_bool(arr[40]), + userDiscoverySharePromotion: dco_decode_bool(arr[41]), + userDiscoveryInitializationError: dco_decode_bool(arr[42]), + askForFriendPromotions: dco_decode_bool(arr[43]), + currentPreKeyIndexStart: dco_decode_i_64(arr[44]), + currentSignedPreKeyIndexStart: dco_decode_i_64(arr[45]), + lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[46]), + hideChangeLog: dco_decode_bool(arr[47]), + hideMemoriesBackupPromo: dco_decode_bool(arr[48]), + updateFcmToken: dco_decode_bool(arr[49]), + canUseLoginTokenForAuth: dco_decode_bool(arr[50]), + twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[51]), + isBackupEnabled: dco_decode_bool(arr[52]), + passwordLessRecovery: + dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[53]), + fcmToken: dco_decode_opt_String(arr[54]), + currentSetupPage: dco_decode_opt_String(arr[55]), + skipSetupPages: dco_decode_bool(arr[56]), + hasZoomed: dco_decode_bool(arr[57]), + ); + } + + @protected + UserConfigApi dco_decode_user_config_api(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 0) + throw Exception('unexpected arr length: expect 0 but see ${arr.length}'); + return UserConfigApi(); + } + @protected BigInt dco_decode_usize(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5912,6 +6101,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return AnyhowException(inner); } + @protected + DateTime sse_decode_Chrono_Utc(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_64(deserializer); + return DateTime.fromMicrosecondsSinceEpoch(inner.toInt(), isUtc: true); + } + @protected Object sse_decode_DartOpaque(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5919,6 +6115,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return decodeDartOpaque(inner, generalizedFrbRustBinding); } + @protected + Map> sse_decode_Map_String_list_String_None( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_record_string_list_string(deserializer); + return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); + } + @protected Map sse_decode_Map_i_64_list_prim_u_8_strict_None( SseDeserializer deserializer, @@ -5997,6 +6202,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8() != 0; } + @protected + DateTime sse_decode_box_autoadd_Chrono_Utc(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_Chrono_Utc(deserializer)); + } + @protected ApiConnectionState sse_decode_box_autoadd_api_connection_state( SseDeserializer deserializer, @@ -6053,6 +6264,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_legacy_signal_encrypt_result(deserializer)); } + @protected + PasswordlessRecoveryConfig + sse_decode_box_autoadd_passwordless_recovery_config( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_passwordless_recovery_config(deserializer)); + } + @protected PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message( SseDeserializer deserializer, @@ -6061,12 +6281,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_prepared_outgoing_message(deserializer)); } + @protected + TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_twonly_safe_backup(deserializer)); + } + @protected int sse_decode_box_autoadd_u_32(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs return (sse_decode_u_32(deserializer)); } + @protected + UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_user_config(deserializer)); + } + @protected double sse_decode_f_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6159,6 +6393,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getPlatformInt64(); } + @protected + LastBackupUploadState sse_decode_last_backup_upload_state( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return LastBackupUploadState.values[inner]; + } + @protected LegacyMigrationReport sse_decode_legacy_migration_report( SseDeserializer deserializer, @@ -6336,6 +6579,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + List<(String, List)> sse_decode_list_record_string_list_string( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = <(String, List)>[]; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_record_string_list_string(deserializer)); + } + return ans_; + } + @protected List sse_decode_list_sql_row(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6360,6 +6617,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + Map>? sse_decode_opt_Map_String_list_String_None( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_Map_String_list_String_None(deserializer)); + } else { + return null; + } + } + @protected String? sse_decode_opt_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6371,6 +6641,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + DateTime? sse_decode_opt_box_autoadd_Chrono_Utc( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_Chrono_Utc(deserializer)); + } else { + return null; + } + } + @protected ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state( SseDeserializer deserializer, @@ -6433,6 +6716,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + PasswordlessRecoveryConfig? + sse_decode_opt_box_autoadd_passwordless_recovery_config( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_passwordless_recovery_config( + deserializer, + )); + } else { + return null; + } + } + @protected PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message( SseDeserializer deserializer, @@ -6446,6 +6745,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_twonly_safe_backup(deserializer)); + } else { + return null; + } + } + @protected int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6457,6 +6769,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + UserConfig? sse_decode_opt_box_autoadd_user_config( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_user_config(deserializer)); + } else { + return null; + } + } + + @protected + List? sse_decode_opt_list_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_list_String(deserializer)); + } else { + return null; + } + } + @protected Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6468,6 +6804,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + PasswordlessRecoveryConfig sse_decode_passwordless_recovery_config( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_email = sse_decode_opt_String(deserializer); + var var_threshold = sse_decode_i_64(deserializer); + var var_serverKeyProtection = sse_decode_opt_list_prim_u_8_strict( + deserializer, + ); + var var_pinUnlockToken = sse_decode_opt_list_prim_u_8_strict(deserializer); + var var_lastServerHeartbeat = sse_decode_opt_box_autoadd_Chrono_Utc( + deserializer, + ); + var var_lastContactHeartbeat = sse_decode_opt_box_autoadd_Chrono_Utc( + deserializer, + ); + var var_encryptedServerKey = sse_decode_opt_list_prim_u_8_strict( + deserializer, + ); + return PasswordlessRecoveryConfig( + email: var_email, + threshold: var_threshold, + serverKeyProtection: var_serverKeyProtection, + pinUnlockToken: var_pinUnlockToken, + lastServerHeartbeat: var_lastServerHeartbeat, + lastContactHeartbeat: var_lastContactHeartbeat, + encryptedServerKey: var_encryptedServerKey, + ); + } + @protected PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6520,6 +6887,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (var_field0, var_field1); } + @protected + (String, List) sse_decode_record_string_list_string( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = sse_decode_String(deserializer); + var var_field1 = sse_decode_list_String(deserializer); + return (var_field0, var_field1); + } + @protected (String, String) sse_decode_record_string_string( SseDeserializer deserializer, @@ -6577,57 +6954,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - ServerResultEmpty sse_decode_server_result_empty( - SseDeserializer deserializer, - ) { + SetupProfile sse_decode_setup_profile(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs - - var tag_ = sse_decode_i_32(deserializer); - switch (tag_) { - case 0: - return ServerResultEmpty_Ok(); - case 1: - var var_field0 = sse_decode_i_32(deserializer); - return ServerResultEmpty_ErrorCode(var_field0); - default: - throw UnimplementedError(''); - } - } - - @protected - ServerResultI64 sse_decode_server_result_i_64(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var tag_ = sse_decode_i_32(deserializer); - switch (tag_) { - case 0: - var var_field0 = sse_decode_i_64(deserializer); - return ServerResultI64_Ok(var_field0); - case 1: - var var_field0 = sse_decode_i_32(deserializer); - return ServerResultI64_ErrorCode(var_field0); - default: - throw UnimplementedError(''); - } - } - - @protected - ServerResultVecU8 sse_decode_server_result_vec_u_8( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var tag_ = sse_decode_i_32(deserializer); - switch (tag_) { - case 0: - var var_field0 = sse_decode_list_prim_u_8_strict(deserializer); - return ServerResultVecU8_Ok(var_field0); - case 1: - var var_field0 = sse_decode_i_32(deserializer); - return ServerResultVecU8_ErrorCode(var_field0); - default: - throw UnimplementedError(''); - } + var inner = sse_decode_i_32(deserializer); + return SetupProfile.values[inner]; } @protected @@ -6675,6 +7005,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + ThemeMode sse_decode_theme_mode(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return ThemeMode.values[inner]; + } + + @protected + TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_lastBackupSize = sse_decode_i_64(deserializer); + var var_backupUploadState = sse_decode_last_backup_upload_state( + deserializer, + ); + var var_lastBackupDone = sse_decode_opt_box_autoadd_Chrono_Utc( + deserializer, + ); + var var_backupId = sse_decode_list_prim_u_8_strict(deserializer); + var var_encryptionKey = sse_decode_list_prim_u_8_strict(deserializer); + return TwonlySafeBackup( + lastBackupSize: var_lastBackupSize, + backupUploadState: var_backupUploadState, + lastBackupDone: var_lastBackupDone, + backupId: var_backupId, + encryptionKey: var_encryptionKey, + ); + } + @protected int sse_decode_u_32(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6705,6 +7063,149 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs } + @protected + UserConfig sse_decode_user_config(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_userId = sse_decode_i_64(deserializer); + var var_username = sse_decode_String(deserializer); + var var_displayName = sse_decode_String(deserializer); + var var_avatarSvg = sse_decode_opt_String(deserializer); + var var_appVersion = sse_decode_i_64(deserializer); + var var_avatarCounter = sse_decode_i_64(deserializer); + var var_videoStabilizationEnabled = sse_decode_bool(deserializer); + var var_isDeveloper = sse_decode_bool(deserializer); + var var_deviceId = sse_decode_i_64(deserializer); + var var_setupProfile = sse_decode_setup_profile(deserializer); + var var_subscriptionPlan = sse_decode_String(deserializer); + var var_subscriptionPlanIdStore = sse_decode_opt_String(deserializer); + var var_lastImageSend = sse_decode_opt_box_autoadd_Chrono_Utc(deserializer); + var var_todaysImageCounter = sse_decode_opt_box_autoadd_i_64(deserializer); + var var_lastPlanBallance = sse_decode_opt_String(deserializer); + var var_additionalUserInvites = sse_decode_opt_String(deserializer); + var var_themeMode = sse_decode_theme_mode(deserializer); + var var_primaryColorValue = sse_decode_opt_box_autoadd_i_64(deserializer); + var var_defaultShowTime = sse_decode_opt_box_autoadd_i_64(deserializer); + var var_requestedAudioPermission = sse_decode_bool(deserializer); + var var_enableDatabaseLogging = sse_decode_bool(deserializer); + var var_automaticallyMarkEqualMediaFilesAsOpened = sse_decode_bool( + deserializer, + ); + var var_showNewsShortcut = sse_decode_bool(deserializer); + var var_showShowImagePreviewWhenSending = sse_decode_bool(deserializer); + var var_startWithCameraOpen = sse_decode_bool(deserializer); + var var_preSelectedEmojies = sse_decode_opt_list_String(deserializer); + var var_autoDownloadOptions = sse_decode_opt_Map_String_list_String_None( + deserializer, + ); + var var_storeMediaFilesInGallery = sse_decode_bool(deserializer); + var var_autoStoreAllSendUnlimitedMediaFiles = sse_decode_bool(deserializer); + var var_typingIndicators = sse_decode_bool(deserializer); + var var_showRestoreFlame = sse_decode_bool(deserializer); + var var_myBestFriendGroupId = sse_decode_opt_String(deserializer); + var var_signalLastSignedPreKeyUpdated = + sse_decode_opt_box_autoadd_Chrono_Utc(deserializer); + var var_signalLastPqcPreKeysUploaded = + sse_decode_opt_box_autoadd_Chrono_Utc(deserializer); + var var_allowErrorTrackingViaSentry = sse_decode_bool(deserializer); + var var_screenLockEnabled = sse_decode_bool(deserializer); + var var_isCloudBackupEnabled = sse_decode_bool(deserializer); + var var_isUserDiscoveryEnabled = sse_decode_bool(deserializer); + var var_requiredSendImages = sse_decode_i_64(deserializer); + var var_userDiscoveryThreshold = sse_decode_u_8(deserializer); + var var_userDiscoveryRequiresManualApproval = sse_decode_bool(deserializer); + var var_userDiscoverySharePromotion = sse_decode_bool(deserializer); + var var_userDiscoveryInitializationError = sse_decode_bool(deserializer); + var var_askForFriendPromotions = sse_decode_bool(deserializer); + var var_currentPreKeyIndexStart = sse_decode_i_64(deserializer); + var var_currentSignedPreKeyIndexStart = sse_decode_i_64(deserializer); + var var_lastChangeLogHash = sse_decode_opt_list_prim_u_8_strict( + deserializer, + ); + var var_hideChangeLog = sse_decode_bool(deserializer); + var var_hideMemoriesBackupPromo = sse_decode_bool(deserializer); + var var_updateFcmToken = sse_decode_bool(deserializer); + var var_canUseLoginTokenForAuth = sse_decode_bool(deserializer); + var var_twonlySafeBackup = sse_decode_opt_box_autoadd_twonly_safe_backup( + deserializer, + ); + var var_isBackupEnabled = sse_decode_bool(deserializer); + var var_passwordLessRecovery = + sse_decode_opt_box_autoadd_passwordless_recovery_config(deserializer); + var var_fcmToken = sse_decode_opt_String(deserializer); + var var_currentSetupPage = sse_decode_opt_String(deserializer); + var var_skipSetupPages = sse_decode_bool(deserializer); + var var_hasZoomed = sse_decode_bool(deserializer); + return UserConfig( + userId: var_userId, + username: var_username, + displayName: var_displayName, + avatarSvg: var_avatarSvg, + appVersion: var_appVersion, + avatarCounter: var_avatarCounter, + videoStabilizationEnabled: var_videoStabilizationEnabled, + isDeveloper: var_isDeveloper, + deviceId: var_deviceId, + setupProfile: var_setupProfile, + subscriptionPlan: var_subscriptionPlan, + subscriptionPlanIdStore: var_subscriptionPlanIdStore, + lastImageSend: var_lastImageSend, + todaysImageCounter: var_todaysImageCounter, + lastPlanBallance: var_lastPlanBallance, + additionalUserInvites: var_additionalUserInvites, + themeMode: var_themeMode, + primaryColorValue: var_primaryColorValue, + defaultShowTime: var_defaultShowTime, + requestedAudioPermission: var_requestedAudioPermission, + enableDatabaseLogging: var_enableDatabaseLogging, + automaticallyMarkEqualMediaFilesAsOpened: + var_automaticallyMarkEqualMediaFilesAsOpened, + showNewsShortcut: var_showNewsShortcut, + showShowImagePreviewWhenSending: var_showShowImagePreviewWhenSending, + startWithCameraOpen: var_startWithCameraOpen, + preSelectedEmojies: var_preSelectedEmojies, + autoDownloadOptions: var_autoDownloadOptions, + storeMediaFilesInGallery: var_storeMediaFilesInGallery, + autoStoreAllSendUnlimitedMediaFiles: + var_autoStoreAllSendUnlimitedMediaFiles, + typingIndicators: var_typingIndicators, + showRestoreFlame: var_showRestoreFlame, + myBestFriendGroupId: var_myBestFriendGroupId, + signalLastSignedPreKeyUpdated: var_signalLastSignedPreKeyUpdated, + signalLastPqcPreKeysUploaded: var_signalLastPqcPreKeysUploaded, + allowErrorTrackingViaSentry: var_allowErrorTrackingViaSentry, + screenLockEnabled: var_screenLockEnabled, + isCloudBackupEnabled: var_isCloudBackupEnabled, + isUserDiscoveryEnabled: var_isUserDiscoveryEnabled, + requiredSendImages: var_requiredSendImages, + userDiscoveryThreshold: var_userDiscoveryThreshold, + userDiscoveryRequiresManualApproval: + var_userDiscoveryRequiresManualApproval, + userDiscoverySharePromotion: var_userDiscoverySharePromotion, + userDiscoveryInitializationError: var_userDiscoveryInitializationError, + askForFriendPromotions: var_askForFriendPromotions, + currentPreKeyIndexStart: var_currentPreKeyIndexStart, + currentSignedPreKeyIndexStart: var_currentSignedPreKeyIndexStart, + lastChangeLogHash: var_lastChangeLogHash, + hideChangeLog: var_hideChangeLog, + hideMemoriesBackupPromo: var_hideMemoriesBackupPromo, + updateFcmToken: var_updateFcmToken, + canUseLoginTokenForAuth: var_canUseLoginTokenForAuth, + twonlySafeBackup: var_twonlySafeBackup, + isBackupEnabled: var_isBackupEnabled, + passwordLessRecovery: var_passwordLessRecovery, + fcmToken: var_fcmToken, + currentSetupPage: var_currentSetupPage, + skipSetupPages: var_skipSetupPages, + hasZoomed: var_hasZoomed, + ); + } + + @protected + UserConfigApi sse_decode_user_config_api(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return UserConfigApi(); + } + @protected BigInt sse_decode_usize(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6720,6 +7221,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.message, serializer); } + @protected + void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_64( + PlatformInt64Util.from(self.microsecondsSinceEpoch), + serializer, + ); + } + @protected void sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( @@ -6735,18 +7245,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - @protected - void sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException( - FutureOr Function(String, bool) self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_DartOpaque( - encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(self), - serializer, - ); - } - @protected void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException( FutureOr Function(String, PlatformInt64) self, @@ -6786,18 +7284,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - @protected - void sse_encode_DartFn_Inputs__Output_unit_AnyhowException( - FutureOr Function() self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_DartOpaque( - encode_DartFn_Inputs__Output_unit_AnyhowException(self), - serializer, - ); - } - @protected void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( FutureOr Function(PlatformInt64) self, @@ -6810,18 +7296,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - @protected - void sse_encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException( - FutureOr Function(PlatformInt64, String, String) self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_DartOpaque( - encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(self), - serializer, - ); - } - @protected void sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( @@ -6870,16 +7344,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - void - sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( - FutureOr Function(PlatformInt64, String?, Uint8List, int) self, + void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException( + FutureOr Function(UserConfig) self, SseSerializer serializer, ) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_DartOpaque( - encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( - self, - ), + encode_DartFn_Inputs_user_config_Output_unit_AnyhowException(self), serializer, ); } @@ -6899,6 +7370,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + void sse_encode_Map_String_list_String_None( + Map> self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_record_string_list_string( + self.entries.map((e) => (e.key, e.value)).toList(), + serializer, + ); + } + @protected void sse_encode_Map_i_64_list_prim_u_8_strict_None( Map self, @@ -6990,6 +7473,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint8(self ? 1 : 0); } + @protected + void sse_encode_box_autoadd_Chrono_Utc( + DateTime self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_Chrono_Utc(self, serializer); + } + @protected void sse_encode_box_autoadd_api_connection_state( ApiConnectionState self, @@ -7056,6 +7548,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_legacy_signal_encrypt_result(self, serializer); } + @protected + void sse_encode_box_autoadd_passwordless_recovery_config( + PasswordlessRecoveryConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_passwordless_recovery_config(self, serializer); + } + @protected void sse_encode_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage self, @@ -7065,12 +7566,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_prepared_outgoing_message(self, serializer); } + @protected + void sse_encode_box_autoadd_twonly_safe_backup( + TwonlySafeBackup self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_twonly_safe_backup(self, serializer); + } + @protected void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_u_32(self, serializer); } + @protected + void sse_encode_box_autoadd_user_config( + UserConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_user_config(self, serializer); + } + @protected void sse_encode_f_64(double self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -7139,6 +7658,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putPlatformInt64(self); } + @protected + void sse_encode_last_backup_upload_state( + LastBackupUploadState self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + @protected void sse_encode_legacy_migration_report( LegacyMigrationReport self, @@ -7302,6 +7830,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_record_string_list_string( + List<(String, List)> self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_record_string_list_string(item, serializer); + } + } + @protected void sse_encode_list_sql_row(List self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -7323,6 +7863,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_Map_String_list_String_None( + Map>? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_Map_String_list_String_None(self, serializer); + } + } + @protected void sse_encode_opt_String(String? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -7333,6 +7886,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_Chrono_Utc( + DateTime? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_Chrono_Utc(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_api_connection_state( ApiConnectionState? self, @@ -7392,6 +7958,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_passwordless_recovery_config( + PasswordlessRecoveryConfig? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_passwordless_recovery_config(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage? self, @@ -7405,6 +7984,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_twonly_safe_backup( + TwonlySafeBackup? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_twonly_safe_backup(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -7415,6 +8007,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_user_config( + UserConfig? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_user_config(self, serializer); + } + } + + @protected + void sse_encode_opt_list_String( + List? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_list_String(self, serializer); + } + } + @protected void sse_encode_opt_list_prim_u_8_strict( Uint8List? self, @@ -7428,6 +8046,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_passwordless_recovery_config( + PasswordlessRecoveryConfig self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_opt_String(self.email, serializer); + sse_encode_i_64(self.threshold, serializer); + sse_encode_opt_list_prim_u_8_strict(self.serverKeyProtection, serializer); + sse_encode_opt_list_prim_u_8_strict(self.pinUnlockToken, serializer); + sse_encode_opt_box_autoadd_Chrono_Utc(self.lastServerHeartbeat, serializer); + sse_encode_opt_box_autoadd_Chrono_Utc( + self.lastContactHeartbeat, + serializer, + ); + sse_encode_opt_list_prim_u_8_strict(self.encryptedServerKey, serializer); + } + @protected void sse_encode_pqc_pre_key_input( PqcPreKeyInput self, @@ -7471,6 +8107,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_i_64(self.$2, serializer); } + @protected + void sse_encode_record_string_list_string( + (String, List) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.$1, serializer); + sse_encode_list_String(self.$2, serializer); + } + @protected void sse_encode_record_string_string( (String, String) self, @@ -7529,50 +8175,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - void sse_encode_server_result_empty( - ServerResultEmpty self, - SseSerializer serializer, - ) { + void sse_encode_setup_profile(SetupProfile self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs - switch (self) { - case ServerResultEmpty_Ok(): - sse_encode_i_32(0, serializer); - case ServerResultEmpty_ErrorCode(field0: final field0): - sse_encode_i_32(1, serializer); - sse_encode_i_32(field0, serializer); - } - } - - @protected - void sse_encode_server_result_i_64( - ServerResultI64 self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - switch (self) { - case ServerResultI64_Ok(field0: final field0): - sse_encode_i_32(0, serializer); - sse_encode_i_64(field0, serializer); - case ServerResultI64_ErrorCode(field0: final field0): - sse_encode_i_32(1, serializer); - sse_encode_i_32(field0, serializer); - } - } - - @protected - void sse_encode_server_result_vec_u_8( - ServerResultVecU8 self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - switch (self) { - case ServerResultVecU8_Ok(field0: final field0): - sse_encode_i_32(0, serializer); - sse_encode_list_prim_u_8_strict(field0, serializer); - case ServerResultVecU8_ErrorCode(field0: final field0): - sse_encode_i_32(1, serializer); - sse_encode_i_32(field0, serializer); - } + sse_encode_i_32(self.index, serializer); } @protected @@ -7608,6 +8213,25 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_list_prim_u_8_strict(self.blobValue, serializer); } + @protected + void sse_encode_theme_mode(ThemeMode self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_twonly_safe_backup( + TwonlySafeBackup self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_64(self.lastBackupSize, serializer); + sse_encode_last_backup_upload_state(self.backupUploadState, serializer); + sse_encode_opt_box_autoadd_Chrono_Utc(self.lastBackupDone, serializer); + sse_encode_list_prim_u_8_strict(self.backupId, serializer); + sse_encode_list_prim_u_8_strict(self.encryptionKey, serializer); + } + @protected void sse_encode_u_32(int self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -7637,6 +8261,92 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs } + @protected + void sse_encode_user_config(UserConfig self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_64(self.userId, serializer); + sse_encode_String(self.username, serializer); + sse_encode_String(self.displayName, serializer); + sse_encode_opt_String(self.avatarSvg, serializer); + sse_encode_i_64(self.appVersion, serializer); + sse_encode_i_64(self.avatarCounter, serializer); + sse_encode_bool(self.videoStabilizationEnabled, serializer); + sse_encode_bool(self.isDeveloper, serializer); + sse_encode_i_64(self.deviceId, serializer); + sse_encode_setup_profile(self.setupProfile, serializer); + sse_encode_String(self.subscriptionPlan, serializer); + sse_encode_opt_String(self.subscriptionPlanIdStore, serializer); + sse_encode_opt_box_autoadd_Chrono_Utc(self.lastImageSend, serializer); + sse_encode_opt_box_autoadd_i_64(self.todaysImageCounter, serializer); + sse_encode_opt_String(self.lastPlanBallance, serializer); + sse_encode_opt_String(self.additionalUserInvites, serializer); + sse_encode_theme_mode(self.themeMode, serializer); + sse_encode_opt_box_autoadd_i_64(self.primaryColorValue, serializer); + sse_encode_opt_box_autoadd_i_64(self.defaultShowTime, serializer); + sse_encode_bool(self.requestedAudioPermission, serializer); + sse_encode_bool(self.enableDatabaseLogging, serializer); + sse_encode_bool(self.automaticallyMarkEqualMediaFilesAsOpened, serializer); + sse_encode_bool(self.showNewsShortcut, serializer); + sse_encode_bool(self.showShowImagePreviewWhenSending, serializer); + sse_encode_bool(self.startWithCameraOpen, serializer); + sse_encode_opt_list_String(self.preSelectedEmojies, serializer); + sse_encode_opt_Map_String_list_String_None( + self.autoDownloadOptions, + serializer, + ); + sse_encode_bool(self.storeMediaFilesInGallery, serializer); + sse_encode_bool(self.autoStoreAllSendUnlimitedMediaFiles, serializer); + sse_encode_bool(self.typingIndicators, serializer); + sse_encode_bool(self.showRestoreFlame, serializer); + sse_encode_opt_String(self.myBestFriendGroupId, serializer); + sse_encode_opt_box_autoadd_Chrono_Utc( + self.signalLastSignedPreKeyUpdated, + serializer, + ); + sse_encode_opt_box_autoadd_Chrono_Utc( + self.signalLastPqcPreKeysUploaded, + serializer, + ); + sse_encode_bool(self.allowErrorTrackingViaSentry, serializer); + sse_encode_bool(self.screenLockEnabled, serializer); + sse_encode_bool(self.isCloudBackupEnabled, serializer); + sse_encode_bool(self.isUserDiscoveryEnabled, serializer); + sse_encode_i_64(self.requiredSendImages, serializer); + sse_encode_u_8(self.userDiscoveryThreshold, serializer); + sse_encode_bool(self.userDiscoveryRequiresManualApproval, serializer); + sse_encode_bool(self.userDiscoverySharePromotion, serializer); + sse_encode_bool(self.userDiscoveryInitializationError, serializer); + sse_encode_bool(self.askForFriendPromotions, serializer); + sse_encode_i_64(self.currentPreKeyIndexStart, serializer); + sse_encode_i_64(self.currentSignedPreKeyIndexStart, serializer); + sse_encode_opt_list_prim_u_8_strict(self.lastChangeLogHash, serializer); + sse_encode_bool(self.hideChangeLog, serializer); + sse_encode_bool(self.hideMemoriesBackupPromo, serializer); + sse_encode_bool(self.updateFcmToken, serializer); + sse_encode_bool(self.canUseLoginTokenForAuth, serializer); + sse_encode_opt_box_autoadd_twonly_safe_backup( + self.twonlySafeBackup, + serializer, + ); + sse_encode_bool(self.isBackupEnabled, serializer); + sse_encode_opt_box_autoadd_passwordless_recovery_config( + self.passwordLessRecovery, + serializer, + ); + sse_encode_opt_String(self.fcmToken, serializer); + sse_encode_opt_String(self.currentSetupPage, serializer); + sse_encode_bool(self.skipSetupPages, serializer); + sse_encode_bool(self.hasZoomed, serializer); + } + + @protected + void sse_encode_user_config_api( + UserConfigApi self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + @protected void sse_encode_usize(BigInt self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart index eaf34ba3..c007a5d1 100644 --- a/lib/core/frb_generated.io.dart +++ b/lib/core/frb_generated.io.dart @@ -8,6 +8,7 @@ import 'bridge.dart'; import 'bridge/api.dart'; import 'bridge/callbacks.dart'; import 'bridge/groups.dart'; +import 'bridge/user_config.dart'; import 'bridge/wrapper.dart'; import 'bridge/wrapper/app_database.dart'; import 'bridge/wrapper/backup.dart'; @@ -23,6 +24,7 @@ import 'keys/backup_password_keys.dart'; import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; import 'signal/engine.dart'; +import 'user_config.dart'; abstract class RustLibApiImplPlatform extends BaseApiImpl { RustLibApiImplPlatform({ @@ -35,16 +37,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException dco_decode_AnyhowException(dynamic raw); + @protected + DateTime dco_decode_Chrono_Utc(dynamic raw); + @protected FutureOr Function(String, String, PlatformInt64, String) dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( dynamic raw, ); - @protected - FutureOr Function(String, bool) - dco_decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(dynamic raw); - @protected FutureOr Function(String, PlatformInt64) dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw); @@ -61,20 +62,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); - @protected - FutureOr Function() - dco_decode_DartFn_Inputs__Output_unit_AnyhowException(dynamic raw); - @protected FutureOr Function(PlatformInt64) dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw); - @protected - FutureOr Function(PlatformInt64, String, String) - dco_decode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException( - dynamic raw, - ); - @protected FutureOr Function(PlatformInt64, Uint8List) dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( @@ -94,14 +85,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - FutureOr Function(PlatformInt64, String?, Uint8List, int) - dco_decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( - dynamic raw, - ); + FutureOr Function(UserConfig) + dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw); @protected Object dco_decode_DartOpaque(dynamic raw); + @protected + Map> dco_decode_Map_String_list_String_None(dynamic raw); + @protected Map dco_decode_Map_i_64_list_prim_u_8_strict_None( dynamic raw, @@ -131,6 +123,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected bool dco_decode_bool(dynamic raw); + @protected + DateTime dco_decode_box_autoadd_Chrono_Utc(dynamic raw); + @protected ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw); @@ -157,14 +152,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + PasswordlessRecoveryConfig + dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw); + @protected PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message( dynamic raw, ); + @protected + TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw); + @protected int dco_decode_box_autoadd_u_32(dynamic raw); + @protected + UserConfig dco_decode_box_autoadd_user_config(dynamic raw); + @protected double dco_decode_f_64(dynamic raw); @@ -189,6 +194,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 dco_decode_isize(dynamic raw); + @protected + LastBackupUploadState dco_decode_last_backup_upload_state(dynamic raw); + @protected LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw); @@ -243,15 +251,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List<(PlatformInt64, Uint8List)> dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw); + @protected + List<(String, List)> dco_decode_list_record_string_list_string( + dynamic raw, + ); + @protected List dco_decode_list_sql_row(dynamic raw); @protected List dco_decode_list_sql_value(dynamic raw); + @protected + Map>? dco_decode_opt_Map_String_list_String_None( + dynamic raw, + ); + @protected String? dco_decode_opt_String(dynamic raw); + @protected + DateTime? dco_decode_opt_box_autoadd_Chrono_Utc(dynamic raw); + @protected ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state( dynamic raw, @@ -270,17 +291,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { LegacySignalEncryptResult? dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw); + @protected + PasswordlessRecoveryConfig? + dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw); + @protected PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message( dynamic raw, ); + @protected + TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw); + @protected int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected + UserConfig? dco_decode_opt_box_autoadd_user_config(dynamic raw); + + @protected + List? dco_decode_opt_list_String(dynamic raw); + @protected Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + @protected + PasswordlessRecoveryConfig dco_decode_passwordless_recovery_config( + dynamic raw, + ); + @protected PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw); @@ -297,6 +336,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + (String, List) dco_decode_record_string_list_string(dynamic raw); + @protected (String, String) dco_decode_record_string_string(dynamic raw); @@ -322,13 +364,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { RustUtils dco_decode_rust_utils(dynamic raw); @protected - ServerResultEmpty dco_decode_server_result_empty(dynamic raw); - - @protected - ServerResultI64 dco_decode_server_result_i_64(dynamic raw); - - @protected - ServerResultVecU8 dco_decode_server_result_vec_u_8(dynamic raw); + SetupProfile dco_decode_setup_profile(dynamic raw); @protected SqlExecutionResult dco_decode_sql_execution_result(dynamic raw); @@ -342,6 +378,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SqlValue dco_decode_sql_value(dynamic raw); + @protected + ThemeMode dco_decode_theme_mode(dynamic raw); + + @protected + TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw); + @protected int dco_decode_u_32(dynamic raw); @@ -357,15 +399,29 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void dco_decode_unit(dynamic raw); + @protected + UserConfig dco_decode_user_config(dynamic raw); + + @protected + UserConfigApi dco_decode_user_config_api(dynamic raw); + @protected BigInt dco_decode_usize(dynamic raw); @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + @protected + DateTime sse_decode_Chrono_Utc(SseDeserializer deserializer); + @protected Object sse_decode_DartOpaque(SseDeserializer deserializer); + @protected + Map> sse_decode_Map_String_list_String_None( + SseDeserializer deserializer, + ); + @protected Map sse_decode_Map_i_64_list_prim_u_8_strict_None( SseDeserializer deserializer, @@ -403,6 +459,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected bool sse_decode_bool(SseDeserializer deserializer); + @protected + DateTime sse_decode_box_autoadd_Chrono_Utc(SseDeserializer deserializer); + @protected ApiConnectionState sse_decode_box_autoadd_api_connection_state( SseDeserializer deserializer, @@ -435,14 +494,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + PasswordlessRecoveryConfig + sse_decode_box_autoadd_passwordless_recovery_config( + SseDeserializer deserializer, + ); + @protected PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message( SseDeserializer deserializer, ); + @protected + TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup( + SseDeserializer deserializer, + ); + @protected int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected + UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer); + @protected double sse_decode_f_64(SseDeserializer deserializer); @@ -469,6 +542,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 sse_decode_isize(SseDeserializer deserializer); + @protected + LastBackupUploadState sse_decode_last_backup_upload_state( + SseDeserializer deserializer, + ); + @protected LegacyMigrationReport sse_decode_legacy_migration_report( SseDeserializer deserializer, @@ -537,15 +615,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + List<(String, List)> sse_decode_list_record_string_list_string( + SseDeserializer deserializer, + ); + @protected List sse_decode_list_sql_row(SseDeserializer deserializer); @protected List sse_decode_list_sql_value(SseDeserializer deserializer); + @protected + Map>? sse_decode_opt_Map_String_list_String_None( + SseDeserializer deserializer, + ); + @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + DateTime? sse_decode_opt_box_autoadd_Chrono_Utc(SseDeserializer deserializer); + @protected ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state( SseDeserializer deserializer, @@ -566,17 +657,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + PasswordlessRecoveryConfig? + sse_decode_opt_box_autoadd_passwordless_recovery_config( + SseDeserializer deserializer, + ); + @protected PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message( SseDeserializer deserializer, ); + @protected + TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup( + SseDeserializer deserializer, + ); + @protected int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected + UserConfig? sse_decode_opt_box_autoadd_user_config( + SseDeserializer deserializer, + ); + + @protected + List? sse_decode_opt_list_String(SseDeserializer deserializer); + @protected Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + PasswordlessRecoveryConfig sse_decode_passwordless_recovery_config( + SseDeserializer deserializer, + ); + @protected PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer); @@ -595,6 +710,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + (String, List) sse_decode_record_string_list_string( + SseDeserializer deserializer, + ); + @protected (String, String) sse_decode_record_string_string( SseDeserializer deserializer, @@ -626,17 +746,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { RustUtils sse_decode_rust_utils(SseDeserializer deserializer); @protected - ServerResultEmpty sse_decode_server_result_empty( - SseDeserializer deserializer, - ); - - @protected - ServerResultI64 sse_decode_server_result_i_64(SseDeserializer deserializer); - - @protected - ServerResultVecU8 sse_decode_server_result_vec_u_8( - SseDeserializer deserializer, - ); + SetupProfile sse_decode_setup_profile(SseDeserializer deserializer); @protected SqlExecutionResult sse_decode_sql_execution_result( @@ -652,6 +762,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SqlValue sse_decode_sql_value(SseDeserializer deserializer); + @protected + ThemeMode sse_decode_theme_mode(SseDeserializer deserializer); + + @protected + TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer); + @protected int sse_decode_u_32(SseDeserializer deserializer); @@ -667,6 +783,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_decode_unit(SseDeserializer deserializer); + @protected + UserConfig sse_decode_user_config(SseDeserializer deserializer); + + @protected + UserConfigApi sse_decode_user_config_api(SseDeserializer deserializer); + @protected BigInt sse_decode_usize(SseDeserializer deserializer); @@ -676,6 +798,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer); + @protected void sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( @@ -683,12 +808,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException( - FutureOr Function(String, bool) self, - SseSerializer serializer, - ); - @protected void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException( FutureOr Function(String, PlatformInt64) self, @@ -708,24 +827,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_DartFn_Inputs__Output_unit_AnyhowException( - FutureOr Function() self, - SseSerializer serializer, - ); - @protected void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( FutureOr Function(PlatformInt64) self, SseSerializer serializer, ); - @protected - void sse_encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException( - FutureOr Function(PlatformInt64, String, String) self, - SseSerializer serializer, - ); - @protected void sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( @@ -750,15 +857,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - void - sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( - FutureOr Function(PlatformInt64, String?, Uint8List, int) self, + void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException( + FutureOr Function(UserConfig) self, SseSerializer serializer, ); @protected void sse_encode_DartOpaque(Object self, SseSerializer serializer); + @protected + void sse_encode_Map_String_list_String_None( + Map> self, + SseSerializer serializer, + ); + @protected void sse_encode_Map_i_64_list_prim_u_8_strict_None( Map self, @@ -801,6 +913,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_bool(bool self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_Chrono_Utc( + DateTime self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_api_connection_state( ApiConnectionState self, @@ -843,15 +961,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_passwordless_recovery_config( + PasswordlessRecoveryConfig self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage self, SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_twonly_safe_backup( + TwonlySafeBackup self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_user_config( + UserConfig self, + SseSerializer serializer, + ); + @protected void sse_encode_f_64(double self, SseSerializer serializer); @@ -882,6 +1018,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); + @protected + void sse_encode_last_backup_upload_state( + LastBackupUploadState self, + SseSerializer serializer, + ); + @protected void sse_encode_legacy_migration_report( LegacyMigrationReport self, @@ -966,15 +1108,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_record_string_list_string( + List<(String, List)> self, + SseSerializer serializer, + ); + @protected void sse_encode_list_sql_row(List self, SseSerializer serializer); @protected void sse_encode_list_sql_value(List self, SseSerializer serializer); + @protected + void sse_encode_opt_Map_String_list_String_None( + Map>? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_Chrono_Utc( + DateTime? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_api_connection_state( ApiConnectionState? self, @@ -999,21 +1159,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_passwordless_recovery_config( + PasswordlessRecoveryConfig? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage? self, SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_twonly_safe_backup( + TwonlySafeBackup? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_user_config( + UserConfig? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_list_String(List? self, SseSerializer serializer); + @protected void sse_encode_opt_list_prim_u_8_strict( Uint8List? self, SseSerializer serializer, ); + @protected + void sse_encode_passwordless_recovery_config( + PasswordlessRecoveryConfig self, + SseSerializer serializer, + ); + @protected void sse_encode_pqc_pre_key_input( PqcPreKeyInput self, @@ -1038,6 +1225,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_record_string_list_string( + (String, List) self, + SseSerializer serializer, + ); + @protected void sse_encode_record_string_string( (String, String) self, @@ -1078,22 +1271,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_rust_utils(RustUtils self, SseSerializer serializer); @protected - void sse_encode_server_result_empty( - ServerResultEmpty self, - SseSerializer serializer, - ); - - @protected - void sse_encode_server_result_i_64( - ServerResultI64 self, - SseSerializer serializer, - ); - - @protected - void sse_encode_server_result_vec_u_8( - ServerResultVecU8 self, - SseSerializer serializer, - ); + void sse_encode_setup_profile(SetupProfile self, SseSerializer serializer); @protected void sse_encode_sql_execution_result( @@ -1110,6 +1288,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_sql_value(SqlValue self, SseSerializer serializer); + @protected + void sse_encode_theme_mode(ThemeMode self, SseSerializer serializer); + + @protected + void sse_encode_twonly_safe_backup( + TwonlySafeBackup self, + SseSerializer serializer, + ); + @protected void sse_encode_u_32(int self, SseSerializer serializer); @@ -1125,6 +1312,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_unit(void self, SseSerializer serializer); + @protected + void sse_encode_user_config(UserConfig self, SseSerializer serializer); + + @protected + void sse_encode_user_config_api(UserConfigApi self, SseSerializer serializer); + @protected void sse_encode_usize(BigInt self, SseSerializer serializer); } diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart index 1f694051..c6bf8050 100644 --- a/lib/core/frb_generated.web.dart +++ b/lib/core/frb_generated.web.dart @@ -11,6 +11,7 @@ import 'bridge.dart'; import 'bridge/api.dart'; import 'bridge/callbacks.dart'; import 'bridge/groups.dart'; +import 'bridge/user_config.dart'; import 'bridge/wrapper.dart'; import 'bridge/wrapper/app_database.dart'; import 'bridge/wrapper/backup.dart'; @@ -25,6 +26,7 @@ import 'keys/backup_password_keys.dart'; import 'lib.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; import 'signal/engine.dart'; +import 'user_config.dart'; abstract class RustLibApiImplPlatform extends BaseApiImpl { RustLibApiImplPlatform({ @@ -37,16 +39,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected AnyhowException dco_decode_AnyhowException(dynamic raw); + @protected + DateTime dco_decode_Chrono_Utc(dynamic raw); + @protected FutureOr Function(String, String, PlatformInt64, String) dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( dynamic raw, ); - @protected - FutureOr Function(String, bool) - dco_decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(dynamic raw); - @protected FutureOr Function(String, PlatformInt64) dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw); @@ -63,20 +64,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); - @protected - FutureOr Function() - dco_decode_DartFn_Inputs__Output_unit_AnyhowException(dynamic raw); - @protected FutureOr Function(PlatformInt64) dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw); - @protected - FutureOr Function(PlatformInt64, String, String) - dco_decode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException( - dynamic raw, - ); - @protected FutureOr Function(PlatformInt64, Uint8List) dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( @@ -96,14 +87,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - FutureOr Function(PlatformInt64, String?, Uint8List, int) - dco_decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( - dynamic raw, - ); + FutureOr Function(UserConfig) + dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw); @protected Object dco_decode_DartOpaque(dynamic raw); + @protected + Map> dco_decode_Map_String_list_String_None(dynamic raw); + @protected Map dco_decode_Map_i_64_list_prim_u_8_strict_None( dynamic raw, @@ -133,6 +125,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected bool dco_decode_bool(dynamic raw); + @protected + DateTime dco_decode_box_autoadd_Chrono_Utc(dynamic raw); + @protected ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw); @@ -159,14 +154,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + PasswordlessRecoveryConfig + dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw); + @protected PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message( dynamic raw, ); + @protected + TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw); + @protected int dco_decode_box_autoadd_u_32(dynamic raw); + @protected + UserConfig dco_decode_box_autoadd_user_config(dynamic raw); + @protected double dco_decode_f_64(dynamic raw); @@ -191,6 +196,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 dco_decode_isize(dynamic raw); + @protected + LastBackupUploadState dco_decode_last_backup_upload_state(dynamic raw); + @protected LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw); @@ -245,15 +253,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List<(PlatformInt64, Uint8List)> dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw); + @protected + List<(String, List)> dco_decode_list_record_string_list_string( + dynamic raw, + ); + @protected List dco_decode_list_sql_row(dynamic raw); @protected List dco_decode_list_sql_value(dynamic raw); + @protected + Map>? dco_decode_opt_Map_String_list_String_None( + dynamic raw, + ); + @protected String? dco_decode_opt_String(dynamic raw); + @protected + DateTime? dco_decode_opt_box_autoadd_Chrono_Utc(dynamic raw); + @protected ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state( dynamic raw, @@ -272,17 +293,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { LegacySignalEncryptResult? dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw); + @protected + PasswordlessRecoveryConfig? + dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw); + @protected PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message( dynamic raw, ); + @protected + TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw); + @protected int? dco_decode_opt_box_autoadd_u_32(dynamic raw); + @protected + UserConfig? dco_decode_opt_box_autoadd_user_config(dynamic raw); + + @protected + List? dco_decode_opt_list_String(dynamic raw); + @protected Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + @protected + PasswordlessRecoveryConfig dco_decode_passwordless_recovery_config( + dynamic raw, + ); + @protected PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw); @@ -299,6 +338,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + (String, List) dco_decode_record_string_list_string(dynamic raw); + @protected (String, String) dco_decode_record_string_string(dynamic raw); @@ -324,13 +366,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { RustUtils dco_decode_rust_utils(dynamic raw); @protected - ServerResultEmpty dco_decode_server_result_empty(dynamic raw); - - @protected - ServerResultI64 dco_decode_server_result_i_64(dynamic raw); - - @protected - ServerResultVecU8 dco_decode_server_result_vec_u_8(dynamic raw); + SetupProfile dco_decode_setup_profile(dynamic raw); @protected SqlExecutionResult dco_decode_sql_execution_result(dynamic raw); @@ -344,6 +380,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SqlValue dco_decode_sql_value(dynamic raw); + @protected + ThemeMode dco_decode_theme_mode(dynamic raw); + + @protected + TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw); + @protected int dco_decode_u_32(dynamic raw); @@ -359,15 +401,29 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void dco_decode_unit(dynamic raw); + @protected + UserConfig dco_decode_user_config(dynamic raw); + + @protected + UserConfigApi dco_decode_user_config_api(dynamic raw); + @protected BigInt dco_decode_usize(dynamic raw); @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + @protected + DateTime sse_decode_Chrono_Utc(SseDeserializer deserializer); + @protected Object sse_decode_DartOpaque(SseDeserializer deserializer); + @protected + Map> sse_decode_Map_String_list_String_None( + SseDeserializer deserializer, + ); + @protected Map sse_decode_Map_i_64_list_prim_u_8_strict_None( SseDeserializer deserializer, @@ -405,6 +461,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected bool sse_decode_bool(SseDeserializer deserializer); + @protected + DateTime sse_decode_box_autoadd_Chrono_Utc(SseDeserializer deserializer); + @protected ApiConnectionState sse_decode_box_autoadd_api_connection_state( SseDeserializer deserializer, @@ -437,14 +496,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + PasswordlessRecoveryConfig + sse_decode_box_autoadd_passwordless_recovery_config( + SseDeserializer deserializer, + ); + @protected PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message( SseDeserializer deserializer, ); + @protected + TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup( + SseDeserializer deserializer, + ); + @protected int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected + UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer); + @protected double sse_decode_f_64(SseDeserializer deserializer); @@ -471,6 +544,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PlatformInt64 sse_decode_isize(SseDeserializer deserializer); + @protected + LastBackupUploadState sse_decode_last_backup_upload_state( + SseDeserializer deserializer, + ); + @protected LegacyMigrationReport sse_decode_legacy_migration_report( SseDeserializer deserializer, @@ -539,15 +617,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + List<(String, List)> sse_decode_list_record_string_list_string( + SseDeserializer deserializer, + ); + @protected List sse_decode_list_sql_row(SseDeserializer deserializer); @protected List sse_decode_list_sql_value(SseDeserializer deserializer); + @protected + Map>? sse_decode_opt_Map_String_list_String_None( + SseDeserializer deserializer, + ); + @protected String? sse_decode_opt_String(SseDeserializer deserializer); + @protected + DateTime? sse_decode_opt_box_autoadd_Chrono_Utc(SseDeserializer deserializer); + @protected ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state( SseDeserializer deserializer, @@ -568,17 +659,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + PasswordlessRecoveryConfig? + sse_decode_opt_box_autoadd_passwordless_recovery_config( + SseDeserializer deserializer, + ); + @protected PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message( SseDeserializer deserializer, ); + @protected + TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup( + SseDeserializer deserializer, + ); + @protected int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); + @protected + UserConfig? sse_decode_opt_box_autoadd_user_config( + SseDeserializer deserializer, + ); + + @protected + List? sse_decode_opt_list_String(SseDeserializer deserializer); + @protected Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + PasswordlessRecoveryConfig sse_decode_passwordless_recovery_config( + SseDeserializer deserializer, + ); + @protected PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer); @@ -597,6 +712,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + (String, List) sse_decode_record_string_list_string( + SseDeserializer deserializer, + ); + @protected (String, String) sse_decode_record_string_string( SseDeserializer deserializer, @@ -628,17 +748,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { RustUtils sse_decode_rust_utils(SseDeserializer deserializer); @protected - ServerResultEmpty sse_decode_server_result_empty( - SseDeserializer deserializer, - ); - - @protected - ServerResultI64 sse_decode_server_result_i_64(SseDeserializer deserializer); - - @protected - ServerResultVecU8 sse_decode_server_result_vec_u_8( - SseDeserializer deserializer, - ); + SetupProfile sse_decode_setup_profile(SseDeserializer deserializer); @protected SqlExecutionResult sse_decode_sql_execution_result( @@ -654,6 +764,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected SqlValue sse_decode_sql_value(SseDeserializer deserializer); + @protected + ThemeMode sse_decode_theme_mode(SseDeserializer deserializer); + + @protected + TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer); + @protected int sse_decode_u_32(SseDeserializer deserializer); @@ -669,6 +785,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_decode_unit(SseDeserializer deserializer); + @protected + UserConfig sse_decode_user_config(SseDeserializer deserializer); + + @protected + UserConfigApi sse_decode_user_config_api(SseDeserializer deserializer); + @protected BigInt sse_decode_usize(SseDeserializer deserializer); @@ -678,6 +800,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer); + @protected void sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( @@ -685,12 +810,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException( - FutureOr Function(String, bool) self, - SseSerializer serializer, - ); - @protected void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException( FutureOr Function(String, PlatformInt64) self, @@ -710,24 +829,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_DartFn_Inputs__Output_unit_AnyhowException( - FutureOr Function() self, - SseSerializer serializer, - ); - @protected void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( FutureOr Function(PlatformInt64) self, SseSerializer serializer, ); - @protected - void sse_encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException( - FutureOr Function(PlatformInt64, String, String) self, - SseSerializer serializer, - ); - @protected void sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( @@ -752,15 +859,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { ); @protected - void - sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( - FutureOr Function(PlatformInt64, String?, Uint8List, int) self, + void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException( + FutureOr Function(UserConfig) self, SseSerializer serializer, ); @protected void sse_encode_DartOpaque(Object self, SseSerializer serializer); + @protected + void sse_encode_Map_String_list_String_None( + Map> self, + SseSerializer serializer, + ); + @protected void sse_encode_Map_i_64_list_prim_u_8_strict_None( Map self, @@ -803,6 +915,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_bool(bool self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_Chrono_Utc( + DateTime self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_api_connection_state( ApiConnectionState self, @@ -845,15 +963,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_passwordless_recovery_config( + PasswordlessRecoveryConfig self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage self, SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_twonly_safe_backup( + TwonlySafeBackup self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_user_config( + UserConfig self, + SseSerializer serializer, + ); + @protected void sse_encode_f_64(double self, SseSerializer serializer); @@ -884,6 +1020,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); + @protected + void sse_encode_last_backup_upload_state( + LastBackupUploadState self, + SseSerializer serializer, + ); + @protected void sse_encode_legacy_migration_report( LegacyMigrationReport self, @@ -968,15 +1110,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_list_record_string_list_string( + List<(String, List)> self, + SseSerializer serializer, + ); + @protected void sse_encode_list_sql_row(List self, SseSerializer serializer); @protected void sse_encode_list_sql_value(List self, SseSerializer serializer); + @protected + void sse_encode_opt_Map_String_list_String_None( + Map>? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_Chrono_Utc( + DateTime? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_api_connection_state( ApiConnectionState? self, @@ -1001,21 +1161,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_passwordless_recovery_config( + PasswordlessRecoveryConfig? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage? self, SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_twonly_safe_backup( + TwonlySafeBackup? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_user_config( + UserConfig? self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_list_String(List? self, SseSerializer serializer); + @protected void sse_encode_opt_list_prim_u_8_strict( Uint8List? self, SseSerializer serializer, ); + @protected + void sse_encode_passwordless_recovery_config( + PasswordlessRecoveryConfig self, + SseSerializer serializer, + ); + @protected void sse_encode_pqc_pre_key_input( PqcPreKeyInput self, @@ -1040,6 +1227,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_record_string_list_string( + (String, List) self, + SseSerializer serializer, + ); + @protected void sse_encode_record_string_string( (String, String) self, @@ -1080,22 +1273,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_rust_utils(RustUtils self, SseSerializer serializer); @protected - void sse_encode_server_result_empty( - ServerResultEmpty self, - SseSerializer serializer, - ); - - @protected - void sse_encode_server_result_i_64( - ServerResultI64 self, - SseSerializer serializer, - ); - - @protected - void sse_encode_server_result_vec_u_8( - ServerResultVecU8 self, - SseSerializer serializer, - ); + void sse_encode_setup_profile(SetupProfile self, SseSerializer serializer); @protected void sse_encode_sql_execution_result( @@ -1112,6 +1290,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_sql_value(SqlValue self, SseSerializer serializer); + @protected + void sse_encode_theme_mode(ThemeMode self, SseSerializer serializer); + + @protected + void sse_encode_twonly_safe_backup( + TwonlySafeBackup self, + SseSerializer serializer, + ); + @protected void sse_encode_u_32(int self, SseSerializer serializer); @@ -1127,6 +1314,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_unit(void self, SseSerializer serializer); + @protected + void sse_encode_user_config(UserConfig self, SseSerializer serializer); + + @protected + void sse_encode_user_config_api(UserConfigApi self, SseSerializer serializer); + @protected void sse_encode_usize(BigInt self, SseSerializer serializer); } diff --git a/lib/core/user_config.dart b/lib/core/user_config.dart new file mode 100644 index 00000000..43aff654 --- /dev/null +++ b/lib/core/user_config.dart @@ -0,0 +1,357 @@ +// 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'; + +enum LastBackupUploadState { + none, + pending, + failed, + success, +} + +class PasswordlessRecoveryConfig { + String? email; + PlatformInt64 threshold; + Uint8List? serverKeyProtection; + Uint8List? pinUnlockToken; + DateTime? lastServerHeartbeat; + DateTime? lastContactHeartbeat; + Uint8List? encryptedServerKey; + + PasswordlessRecoveryConfig({ + this.email, + required this.threshold, + this.serverKeyProtection, + this.pinUnlockToken, + this.lastServerHeartbeat, + this.lastContactHeartbeat, + this.encryptedServerKey, + }); + + @override + int get hashCode => + email.hashCode ^ + threshold.hashCode ^ + serverKeyProtection.hashCode ^ + pinUnlockToken.hashCode ^ + lastServerHeartbeat.hashCode ^ + lastContactHeartbeat.hashCode ^ + encryptedServerKey.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PasswordlessRecoveryConfig && + runtimeType == other.runtimeType && + email == other.email && + threshold == other.threshold && + serverKeyProtection == other.serverKeyProtection && + pinUnlockToken == other.pinUnlockToken && + lastServerHeartbeat == other.lastServerHeartbeat && + lastContactHeartbeat == other.lastContactHeartbeat && + encryptedServerKey == other.encryptedServerKey; +} + +enum SetupProfile { + standard, + customized, +} + +enum ThemeMode { + system, + light, + dark, +} + +class TwonlySafeBackup { + PlatformInt64 lastBackupSize; + LastBackupUploadState backupUploadState; + DateTime? lastBackupDone; + Uint8List backupId; + Uint8List encryptionKey; + + TwonlySafeBackup({ + required this.lastBackupSize, + required this.backupUploadState, + this.lastBackupDone, + required this.backupId, + required this.encryptionKey, + }); + + @override + int get hashCode => + lastBackupSize.hashCode ^ + backupUploadState.hashCode ^ + lastBackupDone.hashCode ^ + backupId.hashCode ^ + encryptionKey.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TwonlySafeBackup && + runtimeType == other.runtimeType && + lastBackupSize == other.lastBackupSize && + backupUploadState == other.backupUploadState && + lastBackupDone == other.lastBackupDone && + backupId == other.backupId && + encryptionKey == other.encryptionKey; +} + +class UserConfig { + PlatformInt64 userId; + String username; + String displayName; + String? avatarSvg; + PlatformInt64 appVersion; + PlatformInt64 avatarCounter; + bool videoStabilizationEnabled; + bool isDeveloper; + PlatformInt64 deviceId; + SetupProfile setupProfile; + String subscriptionPlan; + String? subscriptionPlanIdStore; + DateTime? lastImageSend; + PlatformInt64? todaysImageCounter; + String? lastPlanBallance; + String? additionalUserInvites; + ThemeMode themeMode; + PlatformInt64? primaryColorValue; + PlatformInt64? defaultShowTime; + bool requestedAudioPermission; + bool enableDatabaseLogging; + bool automaticallyMarkEqualMediaFilesAsOpened; + bool showNewsShortcut; + bool showShowImagePreviewWhenSending; + bool startWithCameraOpen; + List? preSelectedEmojies; + Map>? autoDownloadOptions; + bool storeMediaFilesInGallery; + bool autoStoreAllSendUnlimitedMediaFiles; + bool typingIndicators; + bool showRestoreFlame; + String? myBestFriendGroupId; + DateTime? signalLastSignedPreKeyUpdated; + DateTime? signalLastPqcPreKeysUploaded; + bool allowErrorTrackingViaSentry; + bool screenLockEnabled; + bool isCloudBackupEnabled; + bool isUserDiscoveryEnabled; + PlatformInt64 requiredSendImages; + int userDiscoveryThreshold; + bool userDiscoveryRequiresManualApproval; + bool userDiscoverySharePromotion; + bool userDiscoveryInitializationError; + bool askForFriendPromotions; + PlatformInt64 currentPreKeyIndexStart; + PlatformInt64 currentSignedPreKeyIndexStart; + Uint8List? lastChangeLogHash; + bool hideChangeLog; + bool hideMemoriesBackupPromo; + bool updateFcmToken; + bool canUseLoginTokenForAuth; + TwonlySafeBackup? twonlySafeBackup; + bool isBackupEnabled; + PasswordlessRecoveryConfig? passwordLessRecovery; + String? fcmToken; + String? currentSetupPage; + bool skipSetupPages; + bool hasZoomed; + + UserConfig({ + required this.userId, + required this.username, + required this.displayName, + this.avatarSvg, + required this.appVersion, + required this.avatarCounter, + required this.videoStabilizationEnabled, + required this.isDeveloper, + required this.deviceId, + required this.setupProfile, + required this.subscriptionPlan, + this.subscriptionPlanIdStore, + this.lastImageSend, + this.todaysImageCounter, + this.lastPlanBallance, + this.additionalUserInvites, + required this.themeMode, + this.primaryColorValue, + this.defaultShowTime, + required this.requestedAudioPermission, + required this.enableDatabaseLogging, + required this.automaticallyMarkEqualMediaFilesAsOpened, + required this.showNewsShortcut, + required this.showShowImagePreviewWhenSending, + required this.startWithCameraOpen, + this.preSelectedEmojies, + this.autoDownloadOptions, + required this.storeMediaFilesInGallery, + required this.autoStoreAllSendUnlimitedMediaFiles, + required this.typingIndicators, + required this.showRestoreFlame, + this.myBestFriendGroupId, + this.signalLastSignedPreKeyUpdated, + this.signalLastPqcPreKeysUploaded, + required this.allowErrorTrackingViaSentry, + required this.screenLockEnabled, + required this.isCloudBackupEnabled, + required this.isUserDiscoveryEnabled, + required this.requiredSendImages, + required this.userDiscoveryThreshold, + required this.userDiscoveryRequiresManualApproval, + required this.userDiscoverySharePromotion, + required this.userDiscoveryInitializationError, + required this.askForFriendPromotions, + required this.currentPreKeyIndexStart, + required this.currentSignedPreKeyIndexStart, + this.lastChangeLogHash, + required this.hideChangeLog, + required this.hideMemoriesBackupPromo, + required this.updateFcmToken, + required this.canUseLoginTokenForAuth, + this.twonlySafeBackup, + required this.isBackupEnabled, + this.passwordLessRecovery, + this.fcmToken, + this.currentSetupPage, + required this.skipSetupPages, + required this.hasZoomed, + }); + + @override + int get hashCode => + userId.hashCode ^ + username.hashCode ^ + displayName.hashCode ^ + avatarSvg.hashCode ^ + appVersion.hashCode ^ + avatarCounter.hashCode ^ + videoStabilizationEnabled.hashCode ^ + isDeveloper.hashCode ^ + deviceId.hashCode ^ + setupProfile.hashCode ^ + subscriptionPlan.hashCode ^ + subscriptionPlanIdStore.hashCode ^ + lastImageSend.hashCode ^ + todaysImageCounter.hashCode ^ + lastPlanBallance.hashCode ^ + additionalUserInvites.hashCode ^ + themeMode.hashCode ^ + primaryColorValue.hashCode ^ + defaultShowTime.hashCode ^ + requestedAudioPermission.hashCode ^ + enableDatabaseLogging.hashCode ^ + automaticallyMarkEqualMediaFilesAsOpened.hashCode ^ + showNewsShortcut.hashCode ^ + showShowImagePreviewWhenSending.hashCode ^ + startWithCameraOpen.hashCode ^ + preSelectedEmojies.hashCode ^ + autoDownloadOptions.hashCode ^ + storeMediaFilesInGallery.hashCode ^ + autoStoreAllSendUnlimitedMediaFiles.hashCode ^ + typingIndicators.hashCode ^ + showRestoreFlame.hashCode ^ + myBestFriendGroupId.hashCode ^ + signalLastSignedPreKeyUpdated.hashCode ^ + signalLastPqcPreKeysUploaded.hashCode ^ + allowErrorTrackingViaSentry.hashCode ^ + screenLockEnabled.hashCode ^ + isCloudBackupEnabled.hashCode ^ + isUserDiscoveryEnabled.hashCode ^ + requiredSendImages.hashCode ^ + userDiscoveryThreshold.hashCode ^ + userDiscoveryRequiresManualApproval.hashCode ^ + userDiscoverySharePromotion.hashCode ^ + userDiscoveryInitializationError.hashCode ^ + askForFriendPromotions.hashCode ^ + currentPreKeyIndexStart.hashCode ^ + currentSignedPreKeyIndexStart.hashCode ^ + lastChangeLogHash.hashCode ^ + hideChangeLog.hashCode ^ + hideMemoriesBackupPromo.hashCode ^ + updateFcmToken.hashCode ^ + canUseLoginTokenForAuth.hashCode ^ + twonlySafeBackup.hashCode ^ + isBackupEnabled.hashCode ^ + passwordLessRecovery.hashCode ^ + fcmToken.hashCode ^ + currentSetupPage.hashCode ^ + skipSetupPages.hashCode ^ + hasZoomed.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserConfig && + runtimeType == other.runtimeType && + userId == other.userId && + username == other.username && + displayName == other.displayName && + avatarSvg == other.avatarSvg && + appVersion == other.appVersion && + avatarCounter == other.avatarCounter && + videoStabilizationEnabled == other.videoStabilizationEnabled && + isDeveloper == other.isDeveloper && + deviceId == other.deviceId && + setupProfile == other.setupProfile && + subscriptionPlan == other.subscriptionPlan && + subscriptionPlanIdStore == other.subscriptionPlanIdStore && + lastImageSend == other.lastImageSend && + todaysImageCounter == other.todaysImageCounter && + lastPlanBallance == other.lastPlanBallance && + additionalUserInvites == other.additionalUserInvites && + themeMode == other.themeMode && + primaryColorValue == other.primaryColorValue && + defaultShowTime == other.defaultShowTime && + requestedAudioPermission == other.requestedAudioPermission && + enableDatabaseLogging == other.enableDatabaseLogging && + automaticallyMarkEqualMediaFilesAsOpened == + other.automaticallyMarkEqualMediaFilesAsOpened && + showNewsShortcut == other.showNewsShortcut && + showShowImagePreviewWhenSending == + other.showShowImagePreviewWhenSending && + startWithCameraOpen == other.startWithCameraOpen && + preSelectedEmojies == other.preSelectedEmojies && + autoDownloadOptions == other.autoDownloadOptions && + storeMediaFilesInGallery == other.storeMediaFilesInGallery && + autoStoreAllSendUnlimitedMediaFiles == + other.autoStoreAllSendUnlimitedMediaFiles && + typingIndicators == other.typingIndicators && + showRestoreFlame == other.showRestoreFlame && + myBestFriendGroupId == other.myBestFriendGroupId && + signalLastSignedPreKeyUpdated == + other.signalLastSignedPreKeyUpdated && + signalLastPqcPreKeysUploaded == other.signalLastPqcPreKeysUploaded && + allowErrorTrackingViaSentry == other.allowErrorTrackingViaSentry && + screenLockEnabled == other.screenLockEnabled && + isCloudBackupEnabled == other.isCloudBackupEnabled && + isUserDiscoveryEnabled == other.isUserDiscoveryEnabled && + requiredSendImages == other.requiredSendImages && + userDiscoveryThreshold == other.userDiscoveryThreshold && + userDiscoveryRequiresManualApproval == + other.userDiscoveryRequiresManualApproval && + userDiscoverySharePromotion == other.userDiscoverySharePromotion && + userDiscoveryInitializationError == + other.userDiscoveryInitializationError && + askForFriendPromotions == other.askForFriendPromotions && + currentPreKeyIndexStart == other.currentPreKeyIndexStart && + currentSignedPreKeyIndexStart == + other.currentSignedPreKeyIndexStart && + lastChangeLogHash == other.lastChangeLogHash && + hideChangeLog == other.hideChangeLog && + hideMemoriesBackupPromo == other.hideMemoriesBackupPromo && + updateFcmToken == other.updateFcmToken && + canUseLoginTokenForAuth == other.canUseLoginTokenForAuth && + twonlySafeBackup == other.twonlySafeBackup && + isBackupEnabled == other.isBackupEnabled && + passwordLessRecovery == other.passwordLessRecovery && + fcmToken == other.fcmToken && + currentSetupPage == other.currentSetupPage && + skipSetupPages == other.skipSetupPages && + hasZoomed == other.hasZoomed; +} diff --git a/lib/locator.dart b/lib/locator.dart index cdb669ae..82659962 100644 --- a/lib/locator.dart +++ b/lib/locator.dart @@ -5,6 +5,10 @@ import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/news.service.dart'; import 'package:twonly/src/services/user.service.dart'; +export 'package:twonly/core/bridge/api.dart'; +export 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; +export 'package:twonly/src/services/api/rust_api_result.dart'; + final GetIt locator = GetIt.instance; void setupLocator() { diff --git a/lib/main.dart b/lib/main.dart index 5a98ad37..9ad68f61 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,7 +29,6 @@ import 'package:twonly/src/services/memories/memories.service.dart'; import 'package:twonly/src/services/migrations.service.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; -import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/log.dart'; @@ -204,8 +203,6 @@ Future postStartupTasks() async { }), ); - unawaited(UserDiscoveryService.verifyInitializationOnStartup()); - await Future.delayed(const Duration(seconds: 10)); unawaited(initializeBackgroundTaskManager()); // 3. Delayed tasks (Wait for app to settle) diff --git a/lib/src/callbacks/callbacks.dart b/lib/src/callbacks/callbacks.dart index 29b237ba..d700b2a0 100644 --- a/lib/src/callbacks/callbacks.dart +++ b/lib/src/callbacks/callbacks.dart @@ -1,65 +1,25 @@ -import 'dart:typed_data'; - -import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/core/bridge/callbacks.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/callbacks/legacy_signal.callbacks.dart'; import 'package:twonly/src/callbacks/logging.callbacks.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' - as pb; -import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart' - as push_pb; -import 'package:twonly/src/services/api/client2client/errors.c2c.dart'; -import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/flame.service.dart'; -import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.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/session.signal.dart'; +import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/avatars.dart'; -Future _apiCreatePushData( - int contactId, - String? messageId, - Uint8List plaintext, - int messageType, -) async { - final notification = messageType == pb.Message_Type.TEST_NOTIFICATION.value - ? push_pb.PushNotification(kind: push_pb.PushKind.TEST_NOTIFICATION) - : await getPushNotificationFromEncryptedContent( - contactId, - messageId, - pb.EncryptedContent.fromBuffer(plaintext), - ); - if (notification == null) return null; - return encryptPushNotification(contactId, notification); -} - Future _apiMediaAction( String kind, String mediaId, int contactId, String messageId, ) async { - if (kind == 'response') { - await handleMediaRelatedResponseFromReceiver(messageId); - return; - } - if (kind == 'delete') { - final media = await MediaFileService.fromMediaId(mediaId); - media?.fullMediaRemoval(); - return; - } final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); if (media == null) return; switch (kind) { - case 'download': - await startDownloadMedia(media, false); case 'stored': await MediaFileService(media).storeMediaFile(); case 'reupload': @@ -67,26 +27,6 @@ Future _apiMediaAction( } } -Future _apiGroupStateRefresh(String groupId, bool created) async { - if (created) { - await fetchGroupStatesForUnjoinedGroups(); - final group = await twonlyDB.groupsDao.getGroup(groupId); - if (group?.myGroupPrivateKey == null) return; - final key = IdentityKeyPair.fromSerialized(group!.myGroupPrivateKey!); - await sendCipherTextToGroup( - groupId, - pb.EncryptedContent( - groupJoin: pb.EncryptedContent_GroupJoin( - groupPublicKey: key.getPublicKey().serialize(), - ), - ), - ); - return; - } - final group = await twonlyDB.groupsDao.getGroup(groupId); - if (group != null) await fetchGroupState(group); -} - Future initFlutterCallbacksForRust() async { await initFlutterCallbacks( callbackId: isolateCallbackId, @@ -95,31 +35,15 @@ Future initFlutterCallbacksForRust() async { legacySignalEncrypt: LegacySignalCallbacks.encrypt, legacySignalGeneratePrekeys: LegacySignalCallbacks.generatePrekeys, apiResyncSignalSession: handleSessionResync, - apiPushKeyRequested: (contactId) => - setupNotificationWithUsers(forceContact: contactId), - apiGroupMembershipError: (contactId, groupId, relatedReceiptId) => - handleErrorMessage( - contactId, - pb.EncryptedContent_ErrorMessages( - type: pb - .EncryptedContent_ErrorMessages_Type - .GROUP_NOT_FOUND_OR_NOT_A_MEMBER, - relatedReceiptId: relatedReceiptId, - ), - relatedReceiptId, - groupId: groupId, - ), apiMediaAction: _apiMediaAction, apiVerificationProof: KeyVerificationService.handleVerificationProof, - apiCreatePushData: _apiCreatePushData, apiCreatePushAvatars: (contactId) => createPushAvatars(forceForUserId: contactId), - apiRecoveryChanged: PasswordlessRecoveryService.performHeartbeat, apiMediaReceived: (groupId, timestamp) => incFlameCounter( groupId, true, DateTime.fromMillisecondsSinceEpoch(timestamp), ), - apiGroupStateRefresh: _apiGroupStateRefresh, + apiUserConfigChanged: UserService.handleRustUserConfigChanged, ); } diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index f649b2f7..0782e237 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -2,7 +2,6 @@ import 'package:drift/drift.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/log.dart'; part 'contacts.dao.g.dart'; @@ -86,7 +85,6 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { updatedValues.username.present) { final contact = await getContactByUserId(userId).getSingleOrNull(); if (contact != null) { - await updatePushUser(contact); final group = await twonlyDB.groupsDao.getDirectChat(userId); if (group != null) { await twonlyDB.groupsDao.updateGroup( diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart deleted file mode 100644 index 0f31236e..00000000 --- a/lib/src/model/json/userdata.model.dart +++ /dev/null @@ -1,236 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:twonly/src/services/profile.service.dart'; -part 'userdata.model.g.dart'; - -@JsonSerializable() -class UserData { - UserData({ - required this.userId, - required this.username, - required this.displayName, - required this.subscriptionPlan, - required this.currentSetupPage, - required this.appVersion, - }); - factory UserData.fromJson(Map json) => - _$UserDataFromJson(json); - - final int userId; - - // -- USER PROFILE -- - - String username; - String displayName; - String? avatarSvg; - - @JsonKey(defaultValue: 0) - int appVersion = 0; - - @JsonKey(defaultValue: 0) - int avatarCounter = 0; - - @JsonKey(defaultValue: false) - bool isDeveloper = false; - - @JsonKey(defaultValue: 0) - int deviceId = 0; - - @JsonKey(defaultValue: SetupProfile.standard) - SetupProfile setupProfile = SetupProfile.standard; - - // --- SUBSCRIPTION DTA --- - - @JsonKey(defaultValue: 'Free') - String subscriptionPlan; - - String? subscriptionPlanIdStore; - DateTime? lastImageSend; - int? todaysImageCounter; - - String? lastPlanBallance; - String? additionalUserInvites; - - // --- SETTINGS --- - - @JsonKey(defaultValue: ThemeMode.system) - ThemeMode themeMode = ThemeMode.system; - - int? primaryColorValue; - - Color get primaryColor => primaryColorValue != null - ? Color(primaryColorValue!) - : const Color(0xFF57CC99); - - int? defaultShowTime; - - @JsonKey(defaultValue: false) - bool requestedAudioPermission = false; - - @JsonKey(defaultValue: false) - bool enableDatabaseLogging = false; - - @JsonKey(defaultValue: false) - bool automaticallyMarkEqualMediaFilesAsOpened = false; - - @JsonKey(defaultValue: true) - bool videoStabilizationEnabled = true; - - @JsonKey(defaultValue: true) - bool showNewsShortcut = true; - - @JsonKey(defaultValue: false) - bool showShowImagePreviewWhenSending = false; - - @JsonKey(defaultValue: true) - bool startWithCameraOpen = true; - - List? preSelectedEmojies; - - Map>? autoDownloadOptions; - - @JsonKey(defaultValue: true) - bool storeMediaFilesInGallery = true; - - @JsonKey(defaultValue: false) - bool autoStoreAllSendUnlimitedMediaFiles = false; - - @JsonKey(defaultValue: true) - bool typingIndicators = true; - - @JsonKey(defaultValue: true) - bool showRestoreFlame = true; - - String? myBestFriendGroupId; - - DateTime? signalLastSignedPreKeyUpdated; - - DateTime? signalLastPqcPreKeysUploaded; - - @JsonKey(defaultValue: false) - bool allowErrorTrackingViaSentry = false; - - @JsonKey(defaultValue: false) - bool screenLockEnabled = false; - - @JsonKey(defaultValue: false) - bool isCloudBackupEnabled = false; - - // > User Discovery Configurations - - @JsonKey(defaultValue: false) - bool isUserDiscoveryEnabled = false; - - @JsonKey(defaultValue: 4) - int requiredSendImages = 4; - - @JsonKey(defaultValue: 3) - int userDiscoveryThreshold = 3; - - @JsonKey(defaultValue: false) - bool userDiscoveryRequiresManualApproval = false; - - @JsonKey(defaultValue: true) - bool userDiscoverySharePromotion = true; - - @JsonKey(defaultValue: false) - bool userDiscoveryInitializationError = false; - - // -- Custom DATA -- - - @JsonKey(defaultValue: true) - bool askForFriendPromotions = true; - - @JsonKey(defaultValue: 100_000) - int currentPreKeyIndexStart = 100_000; - - @JsonKey(defaultValue: 100_000) - int currentSignedPreKeyIndexStart = 100_000; - - List? lastChangeLogHash; - - @JsonKey(defaultValue: true) - bool hideChangeLog = true; - - @JsonKey(defaultValue: false) - bool hideMemoriesBackupPromo = false; - - @JsonKey(defaultValue: true) - bool updateFCMToken = true; - - @JsonKey(defaultValue: true) - bool canUseLoginTokenForAuth = true; - - // --- BACKUP --- - - @Deprecated('Use the secure storage in rust') - TwonlySafeBackup? twonlySafeBackup; - - @JsonKey(defaultValue: false) - bool isBackupEnabled = false; - - PasswordLessRecovery? passwordLessRecovery; - - // Used for push notifcation via FCM. - String? fcmToken; - - String? currentSetupPage; - - @JsonKey(defaultValue: false) - bool skipSetupPages = false; - - @JsonKey(defaultValue: false) - bool hasZoomed = false; - - Map toJson() => _$UserDataToJson(this); -} - -enum LastBackupUploadState { none, pending, failed, success } - -@JsonSerializable() -class TwonlySafeBackup { - TwonlySafeBackup({ - required this.backupId, - required this.encryptionKey, - }); - factory TwonlySafeBackup.fromJson(Map json) => - _$TwonlySafeBackupFromJson(json); - - @JsonKey(defaultValue: 0) - int lastBackupSize = 0; - - @JsonKey(defaultValue: LastBackupUploadState.none) - LastBackupUploadState backupUploadState = LastBackupUploadState.none; - - DateTime? lastBackupDone; - List backupId; - List encryptionKey; - Map toJson() => _$TwonlySafeBackupToJson(this); -} - -@JsonSerializable() -class PasswordLessRecovery { - PasswordLessRecovery(this.threshold); - - factory PasswordLessRecovery.fromJson(Map json) => - _$PasswordLessRecoveryFromJson(json); - - // Only stored, so the user can see his deposit email address... - String? email; - - // Data shared with trusted friends - - @JsonKey(defaultValue: 2) - int threshold; - // Used to derive the key from the email/pin - List? serverKeyProtection; - List? pinUnlockToken; - // ---> - - // Checking with the server that the server data is valid and not delted throug the pin protection for example. - DateTime? lastServerHeartbeat; - DateTime? lastContactHeartbeat; - List? encryptedServerKey; - - Map toJson() => _$PasswordLessRecoveryToJson(this); -} diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart deleted file mode 100644 index 4dcf11af..00000000 --- a/lib/src/model/json/userdata.model.g.dart +++ /dev/null @@ -1,261 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'userdata.model.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -UserData _$UserDataFromJson(Map json) => - UserData( - userId: (json['userId'] as num).toInt(), - username: json['username'] as String, - displayName: json['displayName'] as String, - subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free', - currentSetupPage: json['currentSetupPage'] as String?, - appVersion: (json['appVersion'] as num?)?.toInt() ?? 0, - ) - ..avatarSvg = json['avatarSvg'] as String? - ..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0 - ..isDeveloper = json['isDeveloper'] as bool? ?? false - ..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0 - ..setupProfile = - $enumDecodeNullable(_$SetupProfileEnumMap, json['setupProfile']) ?? - SetupProfile.standard - ..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String? - ..lastImageSend = json['lastImageSend'] == null - ? null - : DateTime.parse(json['lastImageSend'] as String) - ..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt() - ..lastPlanBallance = json['lastPlanBallance'] as String? - ..additionalUserInvites = json['additionalUserInvites'] as String? - ..themeMode = - $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? - ThemeMode.system - ..primaryColorValue = (json['primaryColorValue'] as num?)?.toInt() - ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() - ..requestedAudioPermission = - json['requestedAudioPermission'] as bool? ?? false - ..enableDatabaseLogging = json['enableDatabaseLogging'] as bool? ?? false - ..automaticallyMarkEqualMediaFilesAsOpened = - json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false - ..videoStabilizationEnabled = - json['videoStabilizationEnabled'] as bool? ?? true - ..showNewsShortcut = json['showNewsShortcut'] as bool? ?? true - ..showShowImagePreviewWhenSending = - json['showShowImagePreviewWhenSending'] as bool? ?? false - ..startWithCameraOpen = json['startWithCameraOpen'] as bool? ?? true - ..preSelectedEmojies = (json['preSelectedEmojies'] as List?) - ?.map((e) => e as String) - .toList() - ..autoDownloadOptions = - (json['autoDownloadOptions'] as Map?)?.map( - (k, e) => MapEntry( - k, - (e as List).map((e) => e as String).toList(), - ), - ) - ..storeMediaFilesInGallery = - json['storeMediaFilesInGallery'] as bool? ?? true - ..autoStoreAllSendUnlimitedMediaFiles = - json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false - ..typingIndicators = json['typingIndicators'] as bool? ?? true - ..showRestoreFlame = json['showRestoreFlame'] as bool? ?? true - ..myBestFriendGroupId = json['myBestFriendGroupId'] as String? - ..signalLastSignedPreKeyUpdated = - json['signalLastSignedPreKeyUpdated'] == null - ? null - : DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String) - ..signalLastPqcPreKeysUploaded = - json['signalLastPqcPreKeysUploaded'] == null - ? null - : DateTime.parse(json['signalLastPqcPreKeysUploaded'] as String) - ..allowErrorTrackingViaSentry = - json['allowErrorTrackingViaSentry'] as bool? ?? false - ..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false - ..isCloudBackupEnabled = json['isCloudBackupEnabled'] as bool? ?? false - ..isUserDiscoveryEnabled = - json['isUserDiscoveryEnabled'] as bool? ?? false - ..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4 - ..userDiscoveryThreshold = - (json['userDiscoveryThreshold'] as num?)?.toInt() ?? 3 - ..userDiscoveryRequiresManualApproval = - json['userDiscoveryRequiresManualApproval'] as bool? ?? false - ..userDiscoverySharePromotion = - json['userDiscoverySharePromotion'] as bool? ?? true - ..userDiscoveryInitializationError = - json['userDiscoveryInitializationError'] as bool? ?? false - ..askForFriendPromotions = json['askForFriendPromotions'] as bool? ?? true - ..currentPreKeyIndexStart = - (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 - ..currentSignedPreKeyIndexStart = - (json['currentSignedPreKeyIndexStart'] as num?)?.toInt() ?? 100000 - ..lastChangeLogHash = (json['lastChangeLogHash'] as List?) - ?.map((e) => (e as num).toInt()) - .toList() - ..hideChangeLog = json['hideChangeLog'] as bool? ?? true - ..hideMemoriesBackupPromo = - json['hideMemoriesBackupPromo'] as bool? ?? false - ..updateFCMToken = json['updateFCMToken'] as bool? ?? true - ..canUseLoginTokenForAuth = - json['canUseLoginTokenForAuth'] as bool? ?? true - ..twonlySafeBackup = json['twonlySafeBackup'] == null - ? null - : TwonlySafeBackup.fromJson( - json['twonlySafeBackup'] as Map, - ) - ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false - ..passwordLessRecovery = json['passwordLessRecovery'] == null - ? null - : PasswordLessRecovery.fromJson( - json['passwordLessRecovery'] as Map, - ) - ..fcmToken = json['fcmToken'] as String? - ..skipSetupPages = json['skipSetupPages'] as bool? ?? false - ..hasZoomed = json['hasZoomed'] as bool? ?? false; - -Map _$UserDataToJson(UserData instance) => { - 'userId': instance.userId, - 'username': instance.username, - 'displayName': instance.displayName, - 'avatarSvg': instance.avatarSvg, - 'appVersion': instance.appVersion, - 'avatarCounter': instance.avatarCounter, - 'isDeveloper': instance.isDeveloper, - 'deviceId': instance.deviceId, - 'setupProfile': _$SetupProfileEnumMap[instance.setupProfile]!, - 'subscriptionPlan': instance.subscriptionPlan, - 'subscriptionPlanIdStore': instance.subscriptionPlanIdStore, - 'lastImageSend': instance.lastImageSend?.toIso8601String(), - 'todaysImageCounter': instance.todaysImageCounter, - 'lastPlanBallance': instance.lastPlanBallance, - 'additionalUserInvites': instance.additionalUserInvites, - 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, - 'primaryColorValue': instance.primaryColorValue, - 'defaultShowTime': instance.defaultShowTime, - 'requestedAudioPermission': instance.requestedAudioPermission, - 'enableDatabaseLogging': instance.enableDatabaseLogging, - 'automaticallyMarkEqualMediaFilesAsOpened': - instance.automaticallyMarkEqualMediaFilesAsOpened, - 'videoStabilizationEnabled': instance.videoStabilizationEnabled, - 'showNewsShortcut': instance.showNewsShortcut, - 'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending, - 'startWithCameraOpen': instance.startWithCameraOpen, - 'preSelectedEmojies': instance.preSelectedEmojies, - 'autoDownloadOptions': instance.autoDownloadOptions, - 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, - 'autoStoreAllSendUnlimitedMediaFiles': - instance.autoStoreAllSendUnlimitedMediaFiles, - 'typingIndicators': instance.typingIndicators, - 'showRestoreFlame': instance.showRestoreFlame, - 'myBestFriendGroupId': instance.myBestFriendGroupId, - 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated - ?.toIso8601String(), - 'signalLastPqcPreKeysUploaded': instance.signalLastPqcPreKeysUploaded - ?.toIso8601String(), - 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, - 'screenLockEnabled': instance.screenLockEnabled, - 'isCloudBackupEnabled': instance.isCloudBackupEnabled, - 'isUserDiscoveryEnabled': instance.isUserDiscoveryEnabled, - 'requiredSendImages': instance.requiredSendImages, - 'userDiscoveryThreshold': instance.userDiscoveryThreshold, - 'userDiscoveryRequiresManualApproval': - instance.userDiscoveryRequiresManualApproval, - 'userDiscoverySharePromotion': instance.userDiscoverySharePromotion, - 'userDiscoveryInitializationError': instance.userDiscoveryInitializationError, - 'askForFriendPromotions': instance.askForFriendPromotions, - 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, - 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, - 'lastChangeLogHash': instance.lastChangeLogHash, - 'hideChangeLog': instance.hideChangeLog, - 'hideMemoriesBackupPromo': instance.hideMemoriesBackupPromo, - 'updateFCMToken': instance.updateFCMToken, - 'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth, - 'twonlySafeBackup': instance.twonlySafeBackup, - 'isBackupEnabled': instance.isBackupEnabled, - 'passwordLessRecovery': instance.passwordLessRecovery, - 'fcmToken': instance.fcmToken, - 'currentSetupPage': instance.currentSetupPage, - 'skipSetupPages': instance.skipSetupPages, - 'hasZoomed': instance.hasZoomed, -}; - -const _$SetupProfileEnumMap = { - SetupProfile.standard: 'standard', - SetupProfile.customized: 'customized', -}; - -const _$ThemeModeEnumMap = { - ThemeMode.system: 'system', - ThemeMode.light: 'light', - ThemeMode.dark: 'dark', -}; - -TwonlySafeBackup _$TwonlySafeBackupFromJson(Map json) => - TwonlySafeBackup( - backupId: (json['backupId'] as List) - .map((e) => (e as num).toInt()) - .toList(), - encryptionKey: (json['encryptionKey'] as List) - .map((e) => (e as num).toInt()) - .toList(), - ) - ..lastBackupSize = (json['lastBackupSize'] as num?)?.toInt() ?? 0 - ..backupUploadState = - $enumDecodeNullable( - _$LastBackupUploadStateEnumMap, - json['backupUploadState'], - ) ?? - LastBackupUploadState.none - ..lastBackupDone = json['lastBackupDone'] == null - ? null - : DateTime.parse(json['lastBackupDone'] as String); - -Map _$TwonlySafeBackupToJson(TwonlySafeBackup instance) => - { - 'lastBackupSize': instance.lastBackupSize, - 'backupUploadState': - _$LastBackupUploadStateEnumMap[instance.backupUploadState]!, - 'lastBackupDone': instance.lastBackupDone?.toIso8601String(), - 'backupId': instance.backupId, - 'encryptionKey': instance.encryptionKey, - }; - -const _$LastBackupUploadStateEnumMap = { - LastBackupUploadState.none: 'none', - LastBackupUploadState.pending: 'pending', - LastBackupUploadState.failed: 'failed', - LastBackupUploadState.success: 'success', -}; - -PasswordLessRecovery _$PasswordLessRecoveryFromJson( - Map json, -) => PasswordLessRecovery((json['threshold'] as num?)?.toInt() ?? 2) - ..email = json['email'] as String? - ..serverKeyProtection = (json['serverKeyProtection'] as List?) - ?.map((e) => (e as num).toInt()) - .toList() - ..pinUnlockToken = (json['pinUnlockToken'] as List?) - ?.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?) - ?.map((e) => (e as num).toInt()) - .toList(); - -Map _$PasswordLessRecoveryToJson( - PasswordLessRecovery instance, -) => { - 'email': instance.email, - 'threshold': instance.threshold, - 'serverKeyProtection': instance.serverKeyProtection, - 'pinUnlockToken': instance.pinUnlockToken, - 'lastServerHeartbeat': instance.lastServerHeartbeat?.toIso8601String(), - 'lastContactHeartbeat': instance.lastContactHeartbeat?.toIso8601String(), - 'encryptedServerKey': instance.encryptedServerKey, -}; diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart index 6acc1416..0cf531dd 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart @@ -1618,7 +1618,7 @@ class Response_UserData extends $pb.GeneratedMessage { create()..mergeFromJson(json, registry); static final $pb.BuilderInfo _i = $pb.BuilderInfo( - _omitMessageNames ? '' : 'Response.UserData', + _omitMessageNames ? '' : 'Response.UserConfig', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart index 311c88bc..413217f4 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart @@ -564,7 +564,7 @@ const Response_PqcBundle$json = { @$core.Deprecated('Use responseDescriptor instead') const Response_UserData$json = { - '1': 'UserData', + '1': 'UserConfig', '2': [ {'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'}, { @@ -868,7 +868,7 @@ const Response_Ok$json = { '3': 5, '4': 1, '5': 11, - '6': '.server_to_client.Response.UserData', + '6': '.server_to_client.Response.UserConfig', '9': 0, '10': 'userdata' }, diff --git a/lib/src/providers/purchases.provider.dart b/lib/src/providers/purchases.provider.dart index 5a09a01d..9700583d 100644 --- a/lib/src/providers/purchases.provider.dart +++ b/lib/src/providers/purchases.provider.dart @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/subscription.keys.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/purchasable_product.model.dart'; import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/user.service.dart'; @@ -107,7 +106,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin { Log.info( 'Force Ipa check was not stopped. Requesting forced check...', ); - await apiService.forceIpaCheck(); + await RustApi.forceIpaCheck(); }); } @@ -181,10 +180,13 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin { Log.info(purchaseDetails.productID); Log.info(purchaseDetails.verificationData.source); } - final res = await apiService.ipaPurchase( - purchaseDetails.productID, - purchaseDetails.verificationData.source, - purchaseDetails.verificationData.serverVerificationData, + final res = await rustApiResult( + RustApi.ipaPurchase( + productId: purchaseDetails.productID, + source: purchaseDetails.verificationData.source, + verificationData: + purchaseDetails.verificationData.serverVerificationData, + ), ); // plan is updated in the apiProvider, as the server updates its states and responses with // an ok authenticated which is processed in the apiProvider... diff --git a/lib/src/providers/settings.provider.dart b/lib/src/providers/settings.provider.dart index 546052db..82c95748 100644 --- a/lib/src/providers/settings.provider.dart +++ b/lib/src/providers/settings.provider.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:twonly/core/user_config.dart' as rust_config; import 'package:twonly/locator.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/visual/themes/light.dart'; @@ -13,8 +14,15 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { void loadSettings() { if (userService.isUserCreated) { - _themeMode = userService.currentUser.themeMode; - _primaryColor = userService.currentUser.primaryColor; + _themeMode = switch (userService.currentUser.themeMode) { + rust_config.ThemeMode.system => ThemeMode.system, + rust_config.ThemeMode.light => ThemeMode.light, + rust_config.ThemeMode.dark => ThemeMode.dark, + }; + final primaryColorValue = userService.currentUser.primaryColorValue; + _primaryColor = primaryColorValue == null + ? defaultPrimaryColor + : Color(primaryColorValue); notifyListeners(); } else { _themeMode = ThemeMode.system; @@ -31,7 +39,13 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { notifyListeners(); - await UserService.update((u) => u.themeMode = newThemeMode); + await UserService.update( + (u) => u.themeMode = switch (newThemeMode) { + ThemeMode.system => rust_config.ThemeMode.system, + ThemeMode.light => rust_config.ThemeMode.light, + ThemeMode.dark => rust_config.ThemeMode.dark, + }, + ); } Future updatePrimaryColor(Color newColor) async { diff --git a/lib/src/services/api/api.service.dart b/lib/src/services/api/api.service.dart index a5ea9588..92fe8253 100644 --- a/lib/src/services/api/api.service.dart +++ b/lib/src/services/api/api.service.dart @@ -1,101 +1,27 @@ -// ignore_for_file: avoid_dynamic_calls, strict_raw_type - import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; -import 'dart:ui' as ui; import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:drift/drift.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; -// ignore: implementation_imports -import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart'; -import 'package:mutex/mutex.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/core/bridge/api.dart' as rust_api; -import 'package:twonly/core/bridge/api.dart'; -import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' - as client; -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/server_to_client.pb.dart' - as server; -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/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/api/server_messages.api.dart'; -import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/services/memories/memories_cloud.service.dart'; import 'package:twonly/src/services/notifications/fcm.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/protocol_state.signal.dart'; -import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/subscription.service.dart'; -import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user_discovery.service.dart'; -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/utils/secure_storage.dart'; -import 'package:web_socket_channel/io.dart'; - -final lockConnecting = Mutex(); -final lockRetransStore = Mutex(); -final lockAuthentication = Mutex(); /// The ApiProvider is responsible for communicating with the server. /// It handles errors and does automatically tries to reconnect on /// errors or network changes. class ApiService { ApiService(); - Future _rustCall(Future promise) async { - try { - final res = await promise; - if (res is ServerResultEmpty) { - final r = res as ServerResultEmpty; - return await r.map( - ok: (_) => Result.success(null), - errorCode: (e) => Result.error(ErrorCode.valueOf(e.field0)), - ); - } else if (res is ServerResultVecU8) { - final r = res as ServerResultVecU8; - return await r.map( - ok: (ok) => Result.success(ok.field0), - errorCode: (e) => Result.error(ErrorCode.valueOf(e.field0)), - ); - } else if (res is ServerResultI64) { - final r = res as ServerResultI64; - return await r.map( - ok: (ok) => Result.success(ok.field0), - errorCode: (e) => Result.error(ErrorCode.valueOf(e.field0)), - ); - } - return Result.success(res); - } catch (e) { - final msg = e.toString(); - final match = RegExp(r'API error code (\\d+)').firstMatch(msg); - if (match != null) { - final code = int.parse(match.group(1)!); - return Result.error(ErrorCode.valueOf(code)); - } - Log.error('Rust API call failed', error: e); - return Result.error(ErrorCode.InternalError); - } - } - final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev-api.twonly.eu'; // final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu'; final String apiSecure = kReleaseMode ? 's' : 's'; @@ -118,128 +44,82 @@ class ApiService { bool appIsOutdated = false; bool isAuthenticated = false; + bool isConnected = false; - // reconnection params - Timer? reconnectionTimer; - int _reconnectionDelay = 5; - - final HashMap> _pendingRequests = - HashMap(); - IOWebSocketChannel? _channel; // ignore: cancel_subscriptions StreamSubscription>? _connectivitySubscription; - Future _connectTo(String apiUrl) async { - if (appIsOutdated) return false; - try { - final channel = IOWebSocketChannel.connect( - Uri.parse(apiUrl), - pingInterval: const Duration(seconds: 30), - ); - - try { - await channel.ready.timeout(const Duration(seconds: 10)); - } catch (e) { - channel.sink.close().ignore(); - rethrow; - } - - _channel = channel; - _channel!.stream.listen(_onData, onDone: _onDone, onError: _onError); - Log.info('websocket connected to $apiUrl'); - return true; - } catch (e) { - _channel = null; - return false; - } - } - // Function is called after the user is authenticated at the server Future onAuthenticated() async { await FcmNotificationService.initFCMAfterAuthenticated(); _connectionStateController.add(true); if (AppState.isInBackgroundTask) { - await retransmitRawBytes(); - await retransmitAllMessages(); + await RustApi.retransmitAllMessages(); await reuploadMediaFiles(); await tryDownloadAllMediaFiles(); } else if (!AppState.isAppInBackground) { - unawaited(retransmitRawBytes()); - unawaited(retransmitAllMessages()); + unawaited(RustApi.retransmitAllMessages()); unawaited(tryDownloadAllMediaFiles()); unawaited(reuploadMediaFiles()); twonlyDB.markUpdated(); unawaited(syncFlameCounters()); - unawaited(setupNotificationWithUsers()); unawaited(SignalIdentityService.onAuthenticated()); resetResyncedUsers(); - resetUserDiscoveryRequestUpdates(); + // resetUserDiscoveryRequestUpdates(); unawaited(fetchGroupStatesForUnjoinedGroups()); unawaited(fetchMissingGroupPublicKey()); - unawaited(checkForDeletedUsernames()); - unawaited(PasswordlessRecoveryService.performHeartbeat()); + unawaited(rust_api.RustApi.checkForDeletedUsernames()); + unawaited(RustApi.performPasswordlessRecoveryHeartbeat()); unawaited(UserDiscoveryService.checkForNewAnnouncedUsers()); memoriesCloudService.init(); } } - Future onConnected() async { - await authenticate(); - _reconnectionDelay = 1; - _connectionStateController.add(true); + Future connect() async { + try { + await rust_api.RustApi.connect(); + for (var attempt = 0; attempt < 100; attempt++) { + final state = await rust_api.RustApi.connectionState(); + isConnected = + state == ApiConnectionState.connected || + state == ApiConnectionState.authenticated; + isAuthenticated = state == ApiConnectionState.authenticated; + if (isAuthenticated || + (!userService.isUserCreated && + state == ApiConnectionState.connected)) { + return true; + } + if (state == ApiConnectionState.permanentlyRejected || + state == ApiConnectionState.suspended) { + return false; + } + await Future.delayed(const Duration(milliseconds: 100)); + } + return false; + } catch (error) { + isConnected = false; + isAuthenticated = false; + Log.error('Rust API connection failed', error: error); + return false; + } } - Future onClosed() async { - if (_channel == null) return; - Log.info('websocket connection closed'); - _channel = null; + Future close(VoidCallback? callback) async { + await rust_api.RustApi.close(); + isConnected = false; isAuthenticated = false; _connectionStateController.add(false); - // Complete all pending requests with null on disconnect - for (final completer in _pendingRequests.values) { - if (!completer.isCompleted) { - completer.complete(null); - } - } - _pendingRequests.clear(); - await twonlyDB.mediaFilesDao.resetPendingDownloadState(); - await startReconnectionTimer(); - } - - Future startReconnectionTimer() async { - if (AppState.isInBackgroundTask) return; - if (reconnectionTimer?.isActive ?? false) { - return; - } - reconnectionTimer?.cancel(); - reconnectionTimer = Timer(Duration(seconds: _reconnectionDelay), () async { - reconnectionTimer = null; - // only try to reconnect in case the app is in the foreground - if (!AppState.isAppInBackground) { - await connect(); - } - }); - _reconnectionDelay = 3; - } - - Future close(Function? callback) async { - Log.info('closing websocket connection'); - if (_channel != null) { - try { - await _channel!.sink.close().timeout(const Duration(seconds: 2)); - } catch (e) { - Log.warn('Timeout or error closing websocket: $e'); - } - await onClosed(); - callback?.call(); - return; - } callback?.call(); } + Future authenticate() async { + await rust_api.RustApi.reloadConfiguration(); + await connect(); + } + Future listenToNetworkChanges() async { if (_connectivitySubscription != null) { return; @@ -253,850 +133,4 @@ class ApiService { // Received changes in available connectivity types! }); } - - Future connect() async { - return lockConnecting.protect(() async { - if (_channel != null) { - return true; - } - // ensure that the connect function is not called again by the timer. - reconnectionTimer?.cancel(); - reconnectionTimer = null; - - isAuthenticated = false; - - final apiUrl = 'ws$apiSecure://$apiHost/api/client'; - - Log.info('connecting to $apiUrl'); - - if (await _connectTo(apiUrl)) { - await onConnected(); - return true; - } - return false; - }); - } - - bool get isConnected => _channel != null && _channel!.closeCode == null; - - Future _onDone() async { - _reconnectionDelay = 3; - await onClosed(); - } - - Future _onError(dynamic e) async { - if (e.toString().contains('Failed host lookup')) { - Log.info('WebSocket connection failed: Host not reachable.'); - } else { - Log.warn('websocket error: $e'); - } - await onClosed(); - } - - Future _onData(dynamic msgBuffer) async { - try { - if (msgBuffer is! Uint8List) { - msgBuffer = Uint8List.fromList(msgBuffer as List); - } - final msg = server.ServerToClient.fromBuffer(msgBuffer); - if (msg.v0.hasResponse()) { - final completer = _pendingRequests.remove(msg.v0.seq); - if (completer != null && !completer.isCompleted) { - completer.complete(msg); - } - unawaited(removeFromRetransmissionBuffer(msg.v0.seq)); - } else { - unawaited(handleServerMessage(msg)); - } - } catch (e) { - Log.error('Error parsing the servers message: $e'); - } - } - - Future _waitForResponse(Int64 seq) async { - final completer = Completer(); - _pendingRequests[seq] = completer; - - final timer = Timer(const Duration(seconds: 60), () { - if (!completer.isCompleted) { - Log.warn('Timeout for message $seq'); - _pendingRequests.remove(seq); - completer.complete(null); - } - }); - - final result = await completer.future; - timer.cancel(); - return result; - } - - Future sendResponse(ClientToServer response) async { - final channel = _channel; - if (channel == null || channel.closeCode != null) { - return false; - } - try { - channel.sink.add(response.writeToBuffer()); - return true; - } catch (e) { - Log.warn('Could not send response to server: $e'); - return false; - } - } - - Future> getRetransmission() async { - return (await KeyValueStore.get('rawbytes-to-retransmit')) ?? {}; - } - - Future retransmitRawBytes() async { - await lockRetransStore.protect(() async { - final retransmit = await getRetransmission(); - if (retransmit.keys.isEmpty) return; - Log.info('retransmitting ${retransmit.keys.length} raw bytes messages'); - var gotError = false; - for (final seq in retransmit.keys) { - try { - _channel!.sink.add(base64Decode(retransmit[seq] as String)); - } catch (e) { - gotError = true; - Log.error('$e'); - } - } - if (!gotError) { - await KeyValueStore.put('rawbytes-to-retransmit', {}); - } - }); - } - - Future addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async { - await lockRetransStore.protect(() async { - final retransmit = await getRetransmission(); - retransmit[seq.toString()] = base64Encode(bytes); - await KeyValueStore.put('rawbytes-to-retransmit', retransmit); - }); - } - - Future removeFromRetransmissionBuffer(Int64 seq) async { - await lockRetransStore.protect(() async { - final retransmit = await getRetransmission(); - if (retransmit.isEmpty) return; - retransmit.remove(seq.toString()); - await KeyValueStore.put('rawbytes-to-retransmit', retransmit); - }); - } - - Future sendRequestSync( - ClientToServer request, { - bool authenticated = true, - bool ensureRetransmission = false, - int? contactId, - }) async { - var seq = Int64(Random().nextInt(4294967296)); - while (_pendingRequests.containsKey(seq)) { - seq = Int64(Random().nextInt(4294967296)); - } - - request.v0.seq = seq; - final requestBytes = request.writeToBuffer(); - - if (ensureRetransmission) { - await addToRetransmissionBuffer(seq, requestBytes); - } - - if (_channel == null) { - Log.warn('sending request while api is not connected'); - if (!await connect()) { - Log.warn('could not connected again'); - return Result.error(ErrorCode.InternalError); - } - if (_channel == null) { - return Result.error(ErrorCode.InternalError); - } - } - - _channel!.sink.add(requestBytes); - - final res = asResult(await _waitForResponse(seq)); - if (res.isSuccess) { - final ok = res.value as server.Response_Ok; - if (ok.hasAuthenticated()) { - final authenticated = ok.authenticated; - await UserService.update((user) { - user.subscriptionPlan = authenticated.plan; - }); - _planUpdateController.add(planFromString(authenticated.plan)); - - // this was triggered by apiService.ipaPurchase, so call the onAuthenticated again - if (isAuthenticated) { - // Trigger the re-upload from images, after Plan change, in case the limit was reached before... - unawaited(finishStartedPreprocessing()); - } - } - } - if (res.isError) { - if (res.error != ErrorCode.ForegroundSessionConnected) { - Log.warn('Got error from server: ${res.error}'); - } - if (res.error == ErrorCode.AppVersionOutdated) { - _appOutdatedController.add(null); - Log.warn('App Version is OUTDATED.'); - appIsOutdated = true; - await close(() {}); - return Result.error(ErrorCode.InternalError); - } - if (res.error == ErrorCode.NewDeviceRegistered) { - _newDeviceRegisteredController.add(null); - Log.warn( - 'Device is disabled, as a newer device restore twonly Backup.', - ); - appIsOutdated = true; - await close(() {}); - return Result.error(ErrorCode.InternalError); - } - if (res.error == ErrorCode.SessionNotAuthenticated) { - if (authenticated) { - await authenticate(); - if (isAuthenticated) { - // this will send the request one more time. - return sendRequestSync(request, authenticated: false); - } else { - Log.warn('Session is not authenticated'); - return Result.error(ErrorCode.InternalError); - } - } - } - if (res.error == ErrorCode.UserIdNotFound && contactId != null) { - Log.warn('Contact deleted their account $contactId.'); - final contact = await twonlyDB.contactsDao - .getContactByUserId(contactId) - .getSingleOrNull(); - if (contact != null) { - await twonlyDB.contactsDao.updateContact( - contactId, - const ContactsCompanion( - accountDeleted: Value(true), - ), - ); - } - await twonlyDB.receiptsDao.deleteReceiptForUser(contactId); - } - } - return res; - } - - Future tryAuthenticateWithToken() async { - final apiAuthToken = await SecureStorage.instance.read( - key: 'api_auth_token', - ); - - if (apiAuthToken != null) { - if (userService.currentUser.appVersion < 62) { - Log.error( - 'DID NOT authenticate the user, as he still has the old version!', - ); - return false; - } - final authenticate = Handshake_Authenticate() - ..userId = Int64(userService.currentUser.userId) - ..appVersion = (await PackageInfo.fromPlatform()).version - ..deviceId = Int64(userService.currentUser.deviceId) - ..inBackground = AppState.isInBackgroundTask - ..authToken = base64Decode(apiAuthToken); - - final handshake = Handshake()..authenticate = authenticate; - final req = createClientToServerFromHandshake(handshake); - - final result = await sendRequestSync(req, authenticated: false); - - if (result.isSuccess) { - Log.info('websocket is authenticated'); - isAuthenticated = true; - if (AppState.isInBackgroundTask) { - await onAuthenticated(); - } else { - unawaited(onAuthenticated()); - - try { - Log.info('Switching authentication to login token'); - final loginToken = await RustKeyManager.getLoginToken(); - final res = await _setLoginToken(loginToken); - if (res.isSuccess) { - Log.info('Switch was successfully.'); - await UserService.update((u) => u.canUseLoginTokenForAuth = true); - await SecureStorage.instance.delete( - key: 'api_auth_token', - ); - } - } catch (e) { - Log.error(e); - } - } - return true; - } - if (result.isError) { - if (result.error != ErrorCode.AuthTokenNotValid && - result.error != ErrorCode.ForegroundSessionConnected) { - Log.error( - 'got error while authenticating to the server: ${result.error}', - ); - return false; - } - } - } - return false; - } - - Future tryAuthenticateWithLoginToken() async { - try { - final loginToken = await RustKeyManager.getLoginToken(); - - final authenticate = Handshake_AuthenticateWithLoginToken() - ..userId = Int64(userService.currentUser.userId) - ..appVersion = (await PackageInfo.fromPlatform()).version - ..deviceId = Int64(userService.currentUser.deviceId) - ..inBackground = AppState.isInBackgroundTask - ..secretLoginToken = loginToken.toList(); - - final handshake = Handshake()..authenticateWithLoginToken = authenticate; - final req = createClientToServerFromHandshake(handshake); - - final result = await sendRequestSync(req, authenticated: false); - - if (result.isSuccess) { - Log.info('websocket is authenticated'); - isAuthenticated = true; - if (AppState.isInBackgroundTask) { - await onAuthenticated(); - } else { - unawaited(onAuthenticated()); - } - return true; - } - if (result.isError) { - if (result.error != ErrorCode.AuthTokenNotValid && - result.error != ErrorCode.ForegroundSessionConnected) { - Log.error( - 'got error while authenticating to the server: ${result.error}', - ); - return false; - } - } - } catch (e) { - Log.error(e); - } - return false; - } - - Future authenticate() async { - return lockAuthentication.protect(() async { - if (isAuthenticated) return; - - if (!userService.isUserCreated) { - return; - } - - if (!userService.isUserCreated) return; - - if (userService.currentUser.canUseLoginTokenForAuth) { - await tryAuthenticateWithLoginToken(); - return; - } - - if (await tryAuthenticateWithToken()) { - return; - } - - final handshake = Handshake() - ..getAuthChallenge = Handshake_GetAuthChallenge(); - final req = createClientToServerFromHandshake(handshake); - - final result = await sendRequestSync(req, authenticated: false); - if (result.isError) { - Log.warn('could not request auth challenge', result); - return; - } - - final challenge = result.value.authchallenge; - - var privKey = (await getSignalIdentityKeyPair())?.getPrivateKey(); - if (privKey == null) return; - final random = getRandomUint8List(32); - final signature = sign( - privKey.serialize(), - challenge as Uint8List, - random, - ); - privKey = null; - - final getAuthToken = Handshake_GetAuthToken() - ..response = signature - ..userId = Int64(userService.currentUser.userId); - - final getauthtoken = Handshake()..getAuthToken = getAuthToken; - - final req2 = createClientToServerFromHandshake(getauthtoken); - - final result2 = await sendRequestSync(req2, authenticated: false); - if (result2.isError) { - Log.error('could not send auth response: ${result2.error}'); - return; - } - - final apiAuthToken = result2.value.authtoken as Uint8List; - final apiAuthTokenB64 = base64Encode(apiAuthToken); - - await SecureStorage.instance.write( - key: 'api_auth_token', - value: apiAuthTokenB64, - ); - - await tryAuthenticateWithToken(); - }); - } - - Future register( - String username, - String? inviteCode, - int proofOfWorkResult, - ) async { - final signalIdentity = await getSignalIdentity(); - if (signalIdentity == null) { - return Result.error(ErrorCode.InternalError); - } - - final signalStore = await getSignalStoreFromIdentity(signalIdentity); - - final signedPreKeysList = await signalStore.loadSignedPreKeys(); - if (signedPreKeysList.isEmpty) { - throw Exception( - 'Signal Signed PreKeys list is empty. Database insertion likely failed due to lack of storage space or a corrupted database.', - ); - } - final signedPreKey = signedPreKeysList[0]; - - final loginToken = await RustKeyManager.getLoginToken(); - - final register = Handshake_Register() - ..username = username - ..publicIdentityKey = (await signalStore.getIdentityKeyPair()) - .getPublicKey() - .serialize() - ..registrationId = Int64(signalIdentity.registrationId) - ..signedPrekey = signedPreKey.getKeyPair().publicKey.serialize() - ..signedPrekeySignature = signedPreKey.signature - ..signedPrekeyId = Int64(signedPreKey.id) - ..langCode = ui.PlatformDispatcher.instance.locale.languageCode - ..loginToken = loginToken - ..proofOfWork = Int64(proofOfWorkResult) - ..isIos = Platform.isIOS; - - if (inviteCode != null && inviteCode != '') { - register.inviteCode = inviteCode; - } - - final handshake = Handshake()..register = register; - final req = createClientToServerFromHandshake(handshake); - - return sendRequestSync(req); - } - - Future checkForDeletedUsernames() async { - final users = await twonlyDB.contactsDao.getContactsByUsername( - '[deleted]', - username2: '[Unknown]', - ); - for (final user in users) { - final userData = await getUserById(user.userId); - if (userData != null) { - await twonlyDB.contactsDao.updateContact( - user.userId, - ContactsCompanion(username: Value(utf8.decode(userData.username))), - ); - } - } - } - - Future getUserById(int userId) async { - final result = await _rustCall( - rust_api.RustApi.getUserById(userId: userId), - ); - if (result.isSuccess && result.value != null) { - return server.Response_UserData.fromBuffer(result.value as List); - } - return null; - } - - Future<(Response_ProofOfWork?, bool)> getProofOfWork() async { - final result = await _rustCall(rust_api.RustApi.getProofOfWork()); - if (result.isSuccess && result.value != null) { - return ( - Response_ProofOfWork.fromBuffer(result.value as List), - false, - ); - } - if (result.isError && result.error == ErrorCode.RegistrationDisabled) { - return (null, true); - } - return (null, false); - } - - Future downloadDone(List token) async { - return _rustCall(rust_api.RustApi.downloadDone(token: token)); - } - - Future _setLoginToken(List token) async { - return _rustCall(rust_api.RustApi.setLoginToken(token: token)); - } - - Future requestMemoriesUpload( - int sizeBytes, - DateTime originalDate, - String mediaId, - ) async { - final get = ApplicationData_RequestMemoriesUpload() - ..size = Int64(sizeBytes) - ..originalDate = Int64(originalDate.millisecondsSinceEpoch) - ..mediaId = mediaId; - final appData = ApplicationData()..requestMemoriesUpload = get; - final req = createClientToServerFromApplicationData(appData); - final res = await sendRequestSync(req); - if (res.isSuccess) { - final ok = res.value as server.Response_Ok; - if (ok.hasMemoriesUploadUrls()) { - return ok.memoriesUploadUrls; - } - } - return null; - } - - Future getMemoriesUsage() async { - final appData = ApplicationData() - ..getMemoriesUsage = ApplicationData_GetMemoriesUsage(); - final req = createClientToServerFromApplicationData(appData); - final res = await sendRequestSync(req); - if (res.isSuccess) { - final ok = res.value as server.Response_Ok; - if (ok.hasMemoriesUsage()) { - return ok.memoriesUsage; - } - } - return null; - } - - Future getMemoriesUrl( - String mediaId, - bool thumbnail, - ) async { - final appData = ApplicationData() - ..getMemoriesUrl = ApplicationData_GetMemoriesUrl( - mediaId: mediaId, - thumbnail: thumbnail, - ); - final req = createClientToServerFromApplicationData(appData); - final res = await sendRequestSync(req); - if (res.isSuccess) { - final ok = res.value as server.Response_Ok; - if (ok.hasMemoriesUrl()) { - return ok.memoriesUrl; - } - } - return null; - } - - Future confirmMemoriesUpload(String mediaId) async { - final get = ApplicationData_ConfirmMemoriesUpload()..mediaId = mediaId; - final appData = ApplicationData()..confirmMemoriesUpload = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - - Future deleteMemory(String mediaId) async { - final get = ApplicationData_DeleteMemory()..mediaId = mediaId; - final appData = ApplicationData()..deleteMemory = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - - Future disableMemoriesBackup() async { - final get = ApplicationData_DisableMemoriesBackup(); - final appData = ApplicationData()..disableMemoriesBackup = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - - Future getUserIdFromUsername(String username) async { - final appData = Handshake( - getUseridByUsername: Handshake_GetUserIdByUsername(username: username), - ); - final req = createClientToServerFromHandshake(appData); - final res = await sendRequestSync(req); - if (res.isSuccess) { - final ok = res.value as server.Response_Ok; - if (ok.hasUserid()) { - return ok.userid.toInt(); - } - } - return null; - } - - Future getUserData(String username) async { - final get = ApplicationData_GetUserByUsername()..username = username; - final appData = ApplicationData()..getUserByUsername = get; - final req = createClientToServerFromApplicationData(appData); - final res = await sendRequestSync(req); - if (res.isSuccess) { - final ok = res.value as server.Response_Ok; - if (ok.hasUserdata()) { - return ok.userdata; - } - } - return null; - } - - Future getPlanBallance() async { - final get = ApplicationData_GetCurrentPlanInfos(); - final appData = ApplicationData()..getCurrentPlanInfos = get; - final req = createClientToServerFromApplicationData(appData); - final res = await sendRequestSync(req); - if (res.isSuccess) { - final ok = res.value as server.Response_Ok; - if (ok.hasPlanballance()) { - return ok.planballance; - } - } - return null; - } - - Future removeAdditionalUser(Int64 userId) async { - final get = ApplicationData_RemoveAdditionalUser()..userId = userId; - final appData = ApplicationData()..removeAdditionalUser = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req, contactId: userId.toInt()); - } - - Future registerPasswordLessRecovery( - List encryptedServerKey, - List? pinUnlockToken, - ) async { - final req = createClientToServerFromApplicationData( - ApplicationData( - registerPasswordlessRecovery: - ApplicationData_RegisterPasswordLessRecovery( - encryptedServerKey: encryptedServerKey, - pinUnlockToken: pinUnlockToken, - ), - ), - ); - return sendRequestSync(req); - } - - Future getServerKeyForPasswordlessRecovery({ - required int userId, - List? serverKeyProtection, - List? pinUnlockToken, - List? pinProtectionKey, - String? email, - }) async { - final get = Handshake_GetServerKeyForPasswordLessRecovery() - ..userId = Int64(userId); - if (serverKeyProtection != null) { - get.serverKeyProtection = serverKeyProtection; - } - 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 submitRecoveryShare({ - required String notificationId, - required List encryptedMessage, - }) async { - final req = createClientToServerFromApplicationData( - ApplicationData( - passwordlessNotification: ApplicationData_PasswordlessNotification( - notificationId: notificationId, - encryptedMessage: encryptedMessage, - ), - ), - ); - return sendRequestSync(req); - } - - Future registerPasswordlessNotification({ - required String notificationId, - required List 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 addAdditionalUser(Int64 userId) async { - final get = ApplicationData_AddAdditionalUser()..userId = userId; - final appData = ApplicationData()..addAdditionalUser = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req, contactId: userId.toInt()); - } - - Future reportUser(int userId, String reason) async { - return _rustCall( - rust_api.RustApi.reportUser(userId: userId, reason: reason), - ); - } - - Future deleteAccount() async { - return _rustCall(rust_api.RustApi.deleteAccount()); - } - - Future updateFCMToken(String googleFcm) async { - final get = ApplicationData_UpdateGoogleFcmToken()..googleFcm = googleFcm; - final appData = ApplicationData()..updateGoogleFcmToken = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - - Future ipaPurchase( - String productId, - String source, - String verificationData, - ) async { - final appData = ApplicationData( - ipaPurchase: ApplicationData_IPAPurchase( - productId: productId, - source: source, - verificationData: verificationData, - ), - ); - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - - Future changeUsername(String username) async { - return _rustCall(rust_api.RustApi.changeUsername(username: username)); - } - - Future forceIpaCheck() async { - return _rustCall(rust_api.RustApi.forceIpaCheck()); - } - - Future updateSignedPreKey( - int signedPreKeyId, - Uint8List signedPreKey, - Uint8List signedPreKeySignature, - ) async { - final get = ApplicationData_UpdateSignedPreKey() - ..signedPrekeyId = Int64(signedPreKeyId) - ..signedPrekey = signedPreKey - ..signedPrekeySignature = signedPreKeySignature; - final appData = ApplicationData()..updateSignedPrekey = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - - Future uploadPqcPreKeys( - int eccSignedPreKeyId, - Uint8List eccSignedPreKey, - Uint8List eccSignedPreKeySignature, - int kyberSignedPreKeyId, - Uint8List kyberSignedPreKey, - Uint8List kyberSignedPreKeySignature, - List prekeys, - ) async { - final get = ApplicationData_UploadPqcPreKeys() - ..eccSignedPrekeyId = Int64(eccSignedPreKeyId) - ..eccSignedPrekey = eccSignedPreKey - ..eccSignedPrekeySignature = eccSignedPreKeySignature - ..kyberSignedPrekeyId = Int64(kyberSignedPreKeyId) - ..kyberSignedPrekey = kyberSignedPreKey - ..kyberSignedPrekeySignature = kyberSignedPreKeySignature - ..prekeys.addAll(prekeys); - final appData = ApplicationData()..uploadPqcPrekeys = get; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req); - } - - Future loadPlanBalance({bool useCache = true}) async { - final ballance = await getPlanBallance(); - if (ballance != null) { - await UserService.update((u) { - u.lastPlanBallance = ballance.writeToJson(); - }); - return ballance; - } - if (userService.currentUser.lastPlanBallance != null && useCache) { - try { - return Response_PlanBallance.fromJson( - userService.currentUser.lastPlanBallance!, - ); - } catch (e) { - Log.error('from json: $e'); - } - } - return ballance; - } - - Future sendTextMessage( - int target, - Uint8List msg, - List? pushData, - ) async { - final testMessage = ApplicationData_TextMessage() - ..userId = Int64(target) - ..body = msg; - - if (pushData != null) { - testMessage.pushData = pushData; - } - final appData = ApplicationData()..textMessage = testMessage; - final req = createClientToServerFromApplicationData(appData); - return sendRequestSync(req, contactId: target); - } - - /// Polls the server for new passwordless recovery notification messages. - /// [alreadyReceivedIds] prevents the server from sending duplicates. - Future - checkForPasswordlessNotification({ - required String notificationId, - required List downloadAuthToken, - List? 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; - } } diff --git a/lib/src/services/api/client2client/additional_data.c2c.dart b/lib/src/services/api/client2client/additional_data.c2c.dart deleted file mode 100644 index ae5c31e4..00000000 --- a/lib/src/services/api/client2client/additional_data.c2c.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:clock/clock.dart' show clock; -import 'package:drift/drift.dart'; -import 'package:twonly/locator.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/services/api/utils.api.dart'; -import 'package:twonly/src/services/key_verification.service.dart'; -import 'package:twonly/src/utils/log.dart'; - -Future handleAdditionalDataMessage( - int fromUserId, - String groupId, - EncryptedContent_AdditionalDataMessage message, - String receiptId, -) async { - Log.info( - '[$receiptId] Got a additional data message: ${message.senderMessageId} from $groupId', - ); - - // Prevent message overwrite: reject if a message with this ID already - // exists from a different sender. - final existing = await twonlyDB.messagesDao - .getMessageById(message.senderMessageId) - .getSingleOrNull(); - if (existing != null && existing.senderId != fromUserId) { - Log.warn( - '[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.', - ); - 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( - MessagesCompanion( - messageId: Value(message.senderMessageId), - senderId: Value(fromUserId), - groupId: Value(groupId), - type: Value(message.type), - additionalMessageData: Value( - Uint8List.fromList(message.additionalMessageData), - ), - createdAt: Value(fromTimestamp(message.timestamp)), - ackByServer: Value(clock.now()), - ), - ); - await twonlyDB.groupsDao.increaseLastMessageExchange( - groupId, - fromTimestamp(message.timestamp), - ); - if (msg != null) { - Log.info( - '[$receiptId] Inserted a new text message with ID: ${msg.messageId}', - ); - } -} diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart deleted file mode 100644 index 706f8545..00000000 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:drift/drift.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/groups.table.dart'; -import 'package:twonly/src/database/twonly.db.dart' hide Message; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/api/utils.api.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; -import 'package:twonly/src/utils/avatars.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; - -Future handleNewContactRequest(int fromUserId) async { - final contact = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - if (contact != null) { - // Either the contact has accepted the fromUserId already: Then just blindly accept the request. - // Or the user has also requested fromUserId. This means that both user have requested each other (while been - // offline for example): In this case the contact can also be accepted blindly. - if (contact.accepted || (!contact.requested && !contact.deletedByUser)) { - if (!contact.accepted) { - // User has also requested the fromUserId, so mark the user as accepted. - await handleContactAccept(fromUserId); - } - - await sendCipherText( - contact.userId, - EncryptedContent( - contactRequest: EncryptedContent_ContactRequest( - type: EncryptedContent_ContactRequest_Type.ACCEPT, - ), - ), - blocking: false, - ); - return true; - } - } - // Request the username by the server so an attacker can not - // forge the displayed username in the contact request - final user = await apiService.getUserById(fromUserId); - if (user == null) { - return false; - } - await twonlyDB.contactsDao.insertOnConflictUpdate( - ContactsCompanion( - username: Value(utf8.decode(user.username)), - userId: Value(fromUserId), - requested: const Value(true), - deletedByUser: const Value(false), - ), - ); - await setupNotificationWithUsers(); - - return true; -} - -Future handleContactAccept(int fromUserId) async { - final contact = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - if (contact == null) return; - if (contact.requested || contact.deletedByUser) { - Log.error('User has never send an request. So ignore the Accept.'); - return; - } - - await twonlyDB.contactsDao.updateContact( - fromUserId, - const ContactsCompanion( - requested: Value(false), - accepted: Value(true), - deletedByUser: Value(false), - ), - ); - await twonlyDB.groupsDao.createNewDirectChat( - fromUserId, - GroupsCompanion( - groupName: Value(getContactDisplayName(contact)), - ), - ); -} - -Future handleContactRequest( - int fromUserId, - EncryptedContent_ContactRequest contactRequest, - String receiptId, -) async { - switch (contactRequest.type) { - case EncryptedContent_ContactRequest_Type.REQUEST: - Log.info('[$receiptId] Got a contact request from $fromUserId'); - return handleNewContactRequest(fromUserId); - case EncryptedContent_ContactRequest_Type.ACCEPT: - Log.info('[$receiptId] Got a contact accept from $fromUserId'); - await handleContactAccept(fromUserId); - case EncryptedContent_ContactRequest_Type.REJECT: - Log.info('[$receiptId] Got a contact reject from $fromUserId'); - await twonlyDB.contactsDao.updateContact( - fromUserId, - const ContactsCompanion( - accepted: Value(false), - requested: Value(false), - deletedByUser: Value(true), - ), - ); - } - return true; -} - -Future handleContactUpdate( - int fromUserId, - EncryptedContent_ContactUpdate contactUpdate, - int? senderProfileCounter, - String receiptId, -) async { - switch (contactUpdate.type) { - case EncryptedContent_ContactUpdate_Type.REQUEST: - Log.info('[$receiptId] Got a contact update request from $fromUserId'); - await sendContactMyProfileData(fromUserId); - - case EncryptedContent_ContactUpdate_Type.UPDATE: - Log.info('[$receiptId] Got a contact update $fromUserId'); - Uint8List? avatarSvgCompressed; - if (contactUpdate.hasAvatarSvgCompressed()) { - avatarSvgCompressed = Uint8List.fromList( - contactUpdate.avatarSvgCompressed, - ); - } - if (contactUpdate.hasDisplayName() && - contactUpdate.hasUsername() && - senderProfileCounter != null) { - final contact = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - - if (contact != null) { - final sharedGroups = await twonlyDB.groupsDao.getGroupsForMember( - fromUserId, - ); - - if (contact.username != contactUpdate.username) { - for (final group in sharedGroups) { - await twonlyDB.groupsDao.insertGroupAction( - GroupHistoriesCompanion( - groupId: Value(group.groupId), - type: const Value(GroupActionType.updatedContactUsername), - contactId: Value(fromUserId), - oldGroupName: Value(contact.username), - newGroupName: Value(contactUpdate.username), - ), - ); - } - } - - if (contact.displayName != contactUpdate.displayName) { - for (final group in sharedGroups) { - await twonlyDB.groupsDao.insertGroupAction( - GroupHistoriesCompanion( - groupId: Value(group.groupId), - type: const Value(GroupActionType.updatedContactDisplayName), - contactId: Value(fromUserId), - oldGroupName: Value(contact.displayName ?? contact.username), - newGroupName: Value(contactUpdate.displayName), - ), - ); - } - } - } - - await twonlyDB.contactsDao.updateContact( - fromUserId, - ContactsCompanion( - avatarSvgCompressed: Value( - avatarSvgCompressed, - ), - displayName: Value(contactUpdate.displayName), - username: Value(contactUpdate.username), - senderProfileCounter: Value(senderProfileCounter), - ), - ); - unawaited(createPushAvatars(forceForUserId: fromUserId)); - } - } -} - -Future handleFlameSync( - String groupId, - EncryptedContent_FlameSync flameSync, - String receiptId, -) async { - Log.info('[$receiptId] Got a flameSync for group $groupId'); - - final group = await twonlyDB.groupsDao.getGroup(groupId); - if (group == null || group.lastFlameCounterChange == null) return; - - var updates = GroupsCompanion( - alsoBestFriend: Value(flameSync.bestFriend), - ); - if (isToday(group.lastFlameCounterChange!) && - isToday(fromTimestamp(flameSync.lastFlameCounterChange)) || - flameSync.forceUpdate) { - if (flameSync.flameCounter > group.flameCounter) { - updates = updates.copyWith( - flameCounter: Value(flameSync.flameCounter.toInt()), - ); - } - if (flameSync.flameCounter > group.maxFlameCounter) { - updates = updates.copyWith( - maxFlameCounter: Value(flameSync.flameCounter.toInt()), - ); - } - } - await twonlyDB.groupsDao.updateGroup(group.groupId, updates); -} - -Future checkForProfileUpdate( - int fromUserId, - EncryptedContent content, -) async { - int? senderProfileCounter; - - if (content.hasSenderProfileCounter()) { - senderProfileCounter = content.senderProfileCounter.toInt(); - if (!content.hasContactUpdate()) { - final contact = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - if (contact != null) { - if (contact.senderProfileCounter < senderProfileCounter) { - await sendCipherText( - fromUserId, - EncryptedContent( - contactUpdate: EncryptedContent_ContactUpdate( - type: EncryptedContent_ContactUpdate_Type.REQUEST, - ), - ), - blocking: false, - ); - } - } - } - } - - return senderProfileCounter; -} diff --git a/lib/src/services/api/client2client/errors.c2c.dart b/lib/src/services/api/client2client/errors.c2c.dart deleted file mode 100644 index f7732231..00000000 --- a/lib/src/services/api/client2client/errors.c2c.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:clock/clock.dart'; -import 'package:drift/drift.dart' show Value; -import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart' - show IdentityKeyPair; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/twonly.db.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'; - -Future handleErrorMessage( - int fromUserId, - EncryptedContent_ErrorMessages error, - String receiptId, { - String? groupId, -}) async { - Log.warn('[$receiptId] Got error from $fromUserId: $error'); - - switch (error.type) { - case EncryptedContent_ErrorMessages_Type - .ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD: - await twonlyDB.receiptsDao.updateReceiptWidthUserId( - fromUserId, - error.relatedReceiptId, - ReceiptsCompanion(markForRetryAfterAccepted: Value(clock.now())), - ); - await twonlyDB.contactsDao.updateContact( - fromUserId, - const ContactsCompanion( - accepted: Value(false), - requested: Value(true), - ), - ); - 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... - 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 updated = await fetchGroupState(group); - if (updated) { - final members = await twonlyDB.groupsDao.getGroupNonLeftMembers( - groupId, - ); - final isStillMember = members.any( - (member) => member.contactId == 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 - default: - break; - } -} diff --git a/lib/src/services/api/client2client/groups.c2c.dart b/lib/src/services/api/client2client/groups.c2c.dart deleted file mode 100644 index b1bc45d8..00000000 --- a/lib/src/services/api/client2client/groups.c2c.dart +++ /dev/null @@ -1,255 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart'; -import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/tables/groups.table.dart'; -import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/api/utils.api.dart'; -import 'package:twonly/src/services/group.service.dart'; -import 'package:twonly/src/utils/log.dart'; - -Future handleGroupCreate( - int fromUserId, - String groupId, - EncryptedContent_GroupCreate newGroup, - String receiptId, -) async { - final user = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - if (user == null) { - // Only contacts can invite other contacts, so this can (via the UI) not happen. - Log.error( - '[$receiptId] User is not a contact. Aborting.', - ); - return; - } - - // 1. Store the new group -> e.g. store the stateKey and groupPublicKey - // 2. Call function that should fetch all jobs - // 1. This function is also called in the main function, in case the state stored on the server could not be loaded - // 2. This function will also send the GroupJoin to all members -> so they get there public key - // 3. Finished - - final myGroupKey = generateIdentityKeyPair(); - - var group = await twonlyDB.groupsDao.getGroup(groupId); - if (group == null) { - // Group state is joinedGroup -> As the current state has not yet been downloaded. - group = await twonlyDB.groupsDao.createNewGroup( - GroupsCompanion( - groupId: Value(groupId), - stateVersionId: const Value(0), - stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), - myGroupPrivateKey: Value(myGroupKey.serialize()), - groupName: Value(newGroup.hasGroupName() ? newGroup.groupName : ''), - joinedGroup: const Value(false), - ), - ); - } 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 updated = await fetchGroupState(group); - if (!updated) { - Log.error( - '[$receiptId] Received group invite/create for $groupId, but failed to fetch group state from server.', - ); - return; - } - // User was already in the group, so update leftGroup back to false - await twonlyDB.groupsDao.updateGroup( - groupId, - GroupsCompanion( - stateVersionId: const Value(0), - stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)), - myGroupPrivateKey: Value(myGroupKey.serialize()), - joinedGroup: const Value(false), - leftGroup: const Value(false), - deletedContent: const Value(false), - ), - ); - } - - if (group == null) { - Log.error( - '[$receiptId] Could not create new group. Probably because the group already existed.', - ); - return; - } - - await twonlyDB.groupsDao.insertGroupAction( - GroupHistoriesCompanion( - groupId: Value(groupId), - contactId: Value(fromUserId), - affectedContactId: const Value(null), - type: const Value(GroupActionType.addMember), - ), - ); - - // Load group members from the server as this is the single source of truth. - // This can be done in the background, so the WebSocket message can be ACKed. - unawaited(fetchGroupStatesForUnjoinedGroups()); - - await sendCipherTextToGroup( - groupId, - EncryptedContent( - groupJoin: EncryptedContent_GroupJoin( - groupPublicKey: myGroupKey.getPublicKey().serialize(), - ), - ), - ); -} - -Future handleGroupUpdate( - int fromUserId, - String groupId, - EncryptedContent_GroupUpdate update, - String receiptId, -) async { - Log.info('[$receiptId] Got group update for $groupId from $fromUserId'); - - final actionType = groupActionTypeFromString(update.groupActionType); - if (actionType == null) { - Log.error( - '[$receiptId] Group action ${update.groupActionType} is unknown ignoring.', - ); - return; - } - - final group = (await twonlyDB.groupsDao.getGroup(groupId))!; - - if (!group.isDirectChat) { - unawaited(fetchGroupState(group)); - } - - switch (actionType) { - case GroupActionType.updatedGroupName: - await twonlyDB.groupsDao.insertGroupAction( - GroupHistoriesCompanion( - groupId: Value(groupId), - type: Value(actionType), - oldGroupName: Value(group.groupName), - newGroupName: Value(update.newGroupName), - contactId: Value(fromUserId), - ), - ); - case GroupActionType.changeDisplayMaxTime: - await twonlyDB.groupsDao.insertGroupAction( - GroupHistoriesCompanion( - groupId: Value(groupId), - type: Value(actionType), - newDeleteMessagesAfterMilliseconds: Value( - update.newDeleteMessagesAfterMilliseconds.toInt(), - ), - contactId: Value(fromUserId), - ), - ); - if (group.isDirectChat) { - await twonlyDB.groupsDao.updateGroup( - group.groupId, - GroupsCompanion( - deleteMessagesAfterMilliseconds: Value( - update.newDeleteMessagesAfterMilliseconds.toInt(), - ), - ), - ); - } - case GroupActionType.removedMember: - case GroupActionType.addMember: - case GroupActionType.leftGroup: - case GroupActionType.promoteToAdmin: - case GroupActionType.demoteToMember: - int? affectedContactId = update.affectedContactId.toInt(); - - if (affectedContactId == userService.currentUser.userId) { - affectedContactId = null; - if (actionType == GroupActionType.removedMember) { - // Oh no, I just got removed from the group... - // This state is handle this case in the fetchGroupState.... - } - } - - await twonlyDB.groupsDao.insertGroupAction( - GroupHistoriesCompanion( - groupId: Value(groupId), - type: Value(actionType), - affectedContactId: Value(affectedContactId), - contactId: Value(fromUserId), - ), - ); - case GroupActionType.createdGroup: - case GroupActionType.updatedContactUsername: - case GroupActionType.updatedContactDisplayName: - break; - } -} - -Future handleGroupJoin( - int fromUserId, - String groupId, - EncryptedContent_GroupJoin join, - String receiptId, -) async { - if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) { - if (!await addNewHiddenContact(fromUserId)) { - Log.error('[$receiptId] Got group join, but could not load contact.'); - // This can happen in case the group join was received before the group create. - // In this case return false, which will cause the receipt to fail and the user - // will resend this message. - return false; - } - } - await twonlyDB.groupsDao.updateMember( - groupId, - fromUserId, - GroupMembersCompanion( - groupPublicKey: Value(Uint8List.fromList(join.groupPublicKey)), - ), - ); - return true; -} - -Future handleResendGroupPublicKey( - int fromUserId, - String groupId, - EncryptedContent_GroupJoin join, - String receiptId, -) async { - final group = await twonlyDB.groupsDao.getGroup(groupId); - if (group == null || group.myGroupPrivateKey == null) return; - final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!); - await sendCipherText( - fromUserId, - EncryptedContent( - groupId: groupId, - groupJoin: EncryptedContent_GroupJoin( - groupPublicKey: keyPair.getPublicKey().serialize(), - ), - ), - blocking: false, - ); -} - -Future handleTypingIndicator( - int fromUserId, - String groupId, - EncryptedContent_TypingIndicator indicator, - String receiptId, -) async { - var lastTypeIndicator = const Value.absent(); - - if (indicator.isTyping) { - lastTypeIndicator = Value(fromTimestamp(indicator.createdAt)); - } - - await twonlyDB.groupsDao.updateMember( - groupId, - fromUserId, - GroupMembersCompanion( - lastChatOpened: Value(fromTimestamp(indicator.createdAt)), - lastTypeIndicator: lastTypeIndicator, - ), - ); -} diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart deleted file mode 100644 index 29ab1a62..00000000 --- a/lib/src/services/api/client2client/media.c2c.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart'; -import 'package:twonly/locator.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/model/protobuf/client/generated/messages.pb.dart' - hide Message; -import 'package:twonly/src/services/api/mediafiles/download.api.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; -import 'package:twonly/src/services/api/utils.api.dart'; -import 'package:twonly/src/services/flame.service.dart'; -import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; -import 'package:twonly/src/utils/log.dart'; - -Future handleMedia( - int fromUserId, - String groupId, - EncryptedContent_Media media, - String receiptId, -) async { - Log.info( - '[$receiptId] Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}', - ); - - late MediaType mediaType; - switch (media.type) { - case EncryptedContent_Media_Type.REUPLOAD: - final message = await twonlyDB.messagesDao - .getMessageById(media.senderMessageId) - .getSingleOrNull(); - if (message == null || - message.senderId != fromUserId || - message.mediaId == null) { - Log.warn( - '[$receiptId] Got reupload for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}', - ); - return false; - } - - // in case there was already a downloaded file delete it... - final mediaService = await MediaFileService.fromMediaId(message.mediaId!); - if (mediaService != null && mediaService.tempPath.existsSync()) { - mediaService.tempPath.deleteSync(); - } - - await twonlyDB.mediaFilesDao.updateMedia( - message.mediaId!, - MediaFilesCompanion( - downloadState: const Value(DownloadState.pending), - downloadToken: Value(Uint8List.fromList(media.downloadToken)), - encryptionKey: Value(Uint8List.fromList(media.encryptionKey)), - encryptionMac: Value(Uint8List.fromList(media.encryptionMac)), - encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)), - ), - ); - - final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( - message.mediaId!, - ); - - if (mediaFile != null) { - unawaited(startDownloadMedia(mediaFile, false)); - } - - return true; - case EncryptedContent_Media_Type.IMAGE: - mediaType = MediaType.image; - case EncryptedContent_Media_Type.VIDEO: - mediaType = MediaType.video; - case EncryptedContent_Media_Type.GIF: - mediaType = MediaType.gif; - case EncryptedContent_Media_Type.AUDIO: - mediaType = MediaType.audio; - } - - var mediaIdValue = const Value.absent(); - - final messageTmp = await twonlyDB.messagesDao - .getMessageById(media.senderMessageId) - .getSingleOrNull(); - if (messageTmp != null) { - if (messageTmp.senderId != fromUserId) { - Log.warn( - '[$receiptId] $fromUserId tried to modify the message from ${messageTmp.senderId}.', - ); - return false; - } - if (messageTmp.mediaId == null) { - Log.warn( - '[$receiptId] This message already exit without a mediaId. Message is dropped.', - ); - return false; - } - final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( - messageTmp.mediaId!, - ); - if (mediaFile?.downloadState != DownloadState.reuploadRequested) { - Log.warn( - '[$receiptId] This message and media file already exit and was not requested again. Dropping it.', - ); - return false; - } - - if (mediaFile != null) { - // media file is reuploaded use the same mediaId - mediaIdValue = Value(mediaFile.mediaId); - } - } - - int? displayLimitInMilliseconds; - if (media.hasDisplayLimitInMilliseconds()) { - if (media.displayLimitInMilliseconds.toInt() < 1000) { - displayLimitInMilliseconds = - media.displayLimitInMilliseconds.toInt() * 1000; - } else { - displayLimitInMilliseconds = media.displayLimitInMilliseconds.toInt(); - } - } - - MediaFile? mediaFile; - Message? message; - - Log.info( - '[$receiptId] Starting transaction for media message ${media.senderMessageId}', - ); - await twonlyDB.transaction(() async { - mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia( - MediaFilesCompanion( - mediaId: mediaIdValue, - downloadState: const Value(DownloadState.pending), - type: Value(mediaType), - requiresAuthentication: Value(media.requiresAuthentication), - displayLimitInMilliseconds: Value( - displayLimitInMilliseconds, - ), - downloadToken: Value(Uint8List.fromList(media.downloadToken)), - encryptionKey: Value(Uint8List.fromList(media.encryptionKey)), - encryptionMac: Value(Uint8List.fromList(media.encryptionMac)), - encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)), - createdAt: Value(fromTimestamp(media.timestamp)), - ), - ); - - if (mediaFile == null) { - Log.error('[$receiptId] Could not insert media file into database'); - return false; - } - - Log.info( - '[$receiptId] Inserting media message: messageId=${media.senderMessageId}, mediaId=${mediaFile!.mediaId}', - ); - - message = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - messageId: Value(media.senderMessageId), - senderId: Value(fromUserId), - groupId: Value(groupId), - mediaId: Value(mediaFile!.mediaId), - type: Value(MessageType.media.name), - additionalMessageData: Value.absentIfNull( - media.hasAdditionalMessageData() - ? Uint8List.fromList(media.additionalMessageData) - : null, - ), - quotesMessageId: Value( - media.hasQuoteMessageId() ? media.quoteMessageId : null, - ), - createdAt: Value(fromTimestamp(media.timestamp)), - ), - ); - }); - Log.info( - '[$receiptId] Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}', - ); - - if (message != null && mediaFile != null) { - await twonlyDB.groupsDao.increaseLastMessageExchange( - groupId, - fromTimestamp(media.timestamp), - ); - Log.info( - '[$receiptId] Inserted a new media message with ID: ${message!.messageId}', - ); - await incFlameCounter( - message!.groupId, - true, - fromTimestamp(media.timestamp), - ); - - unawaited(startDownloadMedia(mediaFile!, false)); - return true; - } else { - if (mediaFile == null && message == null) { - Log.error( - '[$receiptId] Could not insert new message as both the message and mediaFile are empty.', - ); - } else if (mediaFile == null) { - Log.error( - '[$receiptId] Could not insert new message as the mediaFile is empty.', - ); - } else { - Log.error( - '[$receiptId] Could not insert new message as the message is empty.', - ); - } - return false; - } -} - -Future handleMediaUpdate( - int fromUserId, - EncryptedContent_MediaUpdate mediaUpdate, - String receiptId, -) async { - final message = await twonlyDB.messagesDao - .getMessageById(mediaUpdate.targetMessageId) - .getSingleOrNull(); - if (message == null) { - // this can happen, in case the message was already deleted. - Log.info( - '[$receiptId] Got media update to message ${mediaUpdate.targetMessageId} but message not found.', - ); - return; - } - if (message.mediaId == null) { - // this can happen, in case the message was already deleted. - Log.warn( - '[$receiptId] Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.', - ); - return; - } - final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( - message.mediaId!, - ); - if (mediaFile == null) { - Log.info( - '[$receiptId] Got media file update, but media file was not found ${message.mediaId}', - ); - return; - } - - switch (mediaUpdate.type) { - case EncryptedContent_MediaUpdate_Type.REOPENED: - Log.info('[$receiptId] Got media file reopened ${mediaFile.mediaId}'); - await twonlyDB.messagesDao.updateMessageId( - message.messageId, - const MessagesCompanion( - mediaReopened: Value(true), - ), - ); - case EncryptedContent_MediaUpdate_Type.STORED: - Log.info('[$receiptId] Got media file stored ${mediaFile.mediaId}'); - final mediaService = MediaFileService(mediaFile); - await mediaService.storeMediaFile(); - await twonlyDB.messagesDao.updateMessageId( - message.messageId, - const MessagesCompanion( - mediaStored: Value(true), - ), - ); - - case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: - Log.info( - '[$receiptId] Got media file decryption error ${mediaFile.mediaId}', - ); - await reuploadMediaFile(fromUserId, mediaFile, message.messageId); - } -} diff --git a/lib/src/services/api/client2client/messages.c2c.dart b/lib/src/services/api/client2client/messages.c2c.dart deleted file mode 100644 index d6396467..00000000 --- a/lib/src/services/api/client2client/messages.c2c.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:drift/drift.dart' show Value; -import 'package:twonly/locator.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/utils/log.dart'; - -Future handleMessageUpdate( - int contactId, - EncryptedContent_MessageUpdate messageUpdate, - String receiptId, -) async { - switch (messageUpdate.type) { - case EncryptedContent_MessageUpdate_Type.OPENED: - Log.info( - '[$receiptId] Opened message ${messageUpdate.multipleTargetMessageIds}', - ); - try { - await twonlyDB.messagesDao.handleMessagesOpened( - Value(contactId), - messageUpdate.multipleTargetMessageIds, - fromTimestamp(messageUpdate.timestamp), - ); - } catch (e) { - Log.warn('[$receiptId] Error handling messages opened: $e'); - } - case EncryptedContent_MessageUpdate_Type.DELETE: - if (!await isSender( - contactId, - messageUpdate.senderMessageId, - receiptId, - )) { - return; - } - Log.info('[$receiptId] Delete message ${messageUpdate.senderMessageId}'); - try { - await twonlyDB.messagesDao.handleMessageDeletion( - contactId, - messageUpdate.senderMessageId, - fromTimestamp(messageUpdate.timestamp), - ); - } catch (e) { - Log.warn('[$receiptId] Error handling message deletion: $e'); - } - case EncryptedContent_MessageUpdate_Type.EDIT_TEXT: - if (!await isSender( - contactId, - messageUpdate.senderMessageId, - receiptId, - )) { - return; - } - Log.info('[$receiptId] Edit message ${messageUpdate.senderMessageId}'); - try { - await twonlyDB.messagesDao.handleTextEdit( - contactId, - messageUpdate.senderMessageId, - messageUpdate.text, - fromTimestamp(messageUpdate.timestamp), - ); - } catch (e) { - Log.warn('[$receiptId] Error handling text edit: $e'); - } - } -} - -Future isSender( - int fromUserId, - String messageId, - String receiptId, -) async { - final message = await twonlyDB.messagesDao - .getMessageById(messageId) - .getSingleOrNull(); - if (message == null) return false; - if (message.senderId == fromUserId) { - return true; - } - Log.error( - '[$receiptId] Contact $fromUserId tried to modify the message $messageId', - ); - return false; -} diff --git a/lib/src/services/api/client2client/prekeys.c2c.dart b/lib/src/services/api/client2client/prekeys.c2c.dart deleted file mode 100644 index fc0eacda..00000000 --- a/lib/src/services/api/client2client/prekeys.c2c.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:fixnum/fixnum.dart'; -import 'package:twonly/core/bridge/wrapper/signal.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' - as client; -import 'package:twonly/src/services/signal/identity.signal.dart'; - -Future handleRequestNewPreKey() async { - final localPreKeys = await signalGetPreKeys(); - - final prekeysList = []; - for (var i = 0; i < localPreKeys.length; i++) { - prekeysList.add( - client.Response_PreKey() - ..id = Int64(localPreKeys[i].id) - ..prekey = localPreKeys[i].getKeyPair().publicKey.serialize(), - ); - } - final prekeys = client.Response_Prekeys(prekeys: prekeysList); - final ok = client.Response_Ok()..prekeys = prekeys; - return client.Response()..ok = ok; -} - -Future handleRequestNewPqcPreKey() async { - final pqcKeys = await RustSignal.generatePqcPrekeys(); - if (pqcKeys.isEmpty) return null; - - final prekeysList = []; - for (final pqcKey in pqcKeys) { - prekeysList.add( - client.ApplicationData_PqcPreKey( - eccPreKeyId: Int64(pqcKey.eccPreKeyId), - eccPreKey: pqcKey.eccPreKey, - kyberPreKeyId: Int64(pqcKey.kyberPreKeyId), - kyberPreKey: pqcKey.kyberPreKey, - kyberPreKeySignature: pqcKey.kyberPreKeySignature, - ), - ); - } - - final prekeys = client.Response_PqcPrekeys(prekeys: prekeysList); - final ok = client.Response_Ok()..prekeysPqc = prekeys; - return client.Response()..ok = ok; -} diff --git a/lib/src/services/api/client2client/pushkeys.c2c.dart b/lib/src/services/api/client2client/pushkeys.c2c.dart deleted file mode 100644 index 8d6bfcfb..00000000 --- a/lib/src/services/api/client2client/pushkeys.c2c.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:async'; - -import 'package:clock/clock.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; -import 'package:twonly/src/utils/log.dart'; - -DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); - -Future handlePushKey( - int contactId, - EncryptedContent_PushKeys pushKeys, - String receiptId, -) async { - switch (pushKeys.type) { - case EncryptedContent_PushKeys_Type.REQUEST: - Log.info('[$receiptId] Got a pushkey request from $contactId'); - if (lastPushKeyRequest.isBefore( - clock.now().subtract(const Duration(seconds: 60)), - )) { - lastPushKeyRequest = clock.now(); - unawaited(setupNotificationWithUsers(forceContact: contactId)); - } - - case EncryptedContent_PushKeys_Type.UPDATE: - Log.info('[$receiptId] Got a pushkey update from $contactId'); - await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key); - } -} diff --git a/lib/src/services/api/client2client/reaction.c2c.dart b/lib/src/services/api/client2client/reaction.c2c.dart deleted file mode 100644 index 03a423c1..00000000 --- a/lib/src/services/api/client2client/reaction.c2c.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:clock/clock.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; -import 'package:twonly/src/utils/log.dart'; - -Future handleReaction( - int fromUserId, - String groupId, - EncryptedContent_Reaction reaction, - String receiptId, -) async { - Log.info( - '[$receiptId] Got a reaction from for ${reaction.targetMessageId} (remove=${reaction.remove})', - ); - - await twonlyDB.reactionsDao.updateReaction( - fromUserId, - reaction.targetMessageId, - groupId, - reaction.emoji, - reaction.remove, - ); - - await handleMediaRelatedResponseFromReceiver(reaction.targetMessageId); - - if (!reaction.remove) { - await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now()); - } -} diff --git a/lib/src/services/api/client2client/text_message.c2c.dart b/lib/src/services/api/client2client/text_message.c2c.dart deleted file mode 100644 index e6d0e2d0..00000000 --- a/lib/src/services/api/client2client/text_message.c2c.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:clock/clock.dart'; -import 'package:drift/drift.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/tables/messages.table.dart'; -import 'package:twonly/src/database/twonly.db.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/utils/log.dart'; - -Future handleTextMessage( - int fromUserId, - String groupId, - EncryptedContent_TextMessage textMessage, - String receiptId, -) async { - Log.info( - '[$receiptId] Got a text message: ${textMessage.senderMessageId} from $groupId', - ); - - // Prevent message overwrite: reject if a message with this ID already - // exists from a different sender. - final existing = await twonlyDB.messagesDao - .getMessageById(textMessage.senderMessageId) - .getSingleOrNull(); - if (existing != null && existing.senderId != fromUserId) { - Log.warn( - '[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.', - ); - return false; - } - - final message = await twonlyDB.messagesDao.insertMessage( - MessagesCompanion( - messageId: Value(textMessage.senderMessageId), - senderId: Value(fromUserId), - groupId: Value(groupId), - content: Value(textMessage.text), - type: Value(MessageType.text.name), - quotesMessageId: Value( - textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null, - ), - createdAt: Value(fromTimestamp(textMessage.timestamp)), - ackByServer: Value(clock.now()), - ), - ); - await twonlyDB.groupsDao.increaseLastMessageExchange( - groupId, - fromTimestamp(textMessage.timestamp), - ); - if (message != null) { - Log.info( - '[$receiptId] Inserted a new text message with ID: ${message.messageId}', - ); - } - return message != null; -} diff --git a/lib/src/services/api/client2client/user_discovery.c2c.dart b/lib/src/services/api/client2client/user_discovery.c2c.dart deleted file mode 100644 index d784aa56..00000000 --- a/lib/src/services/api/client2client/user_discovery.c2c.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:typed_data'; - -import 'package:twonly/locator.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/user_discovery.service.dart'; -import 'package:twonly/src/utils/log.dart'; - -final _requestedUpdates = {}; - -void resetUserDiscoveryRequestUpdates() { - _requestedUpdates.clear(); -} - -Future checkForUserDiscoveryChanges( - int fromUserId, - List receivedVersion, - String receiptId, -) async { - Log.info('[$receiptId] Checking for a new user discovery version.'); - final currentVersion = await UserDiscoveryService.shouldRequestNewMessages( - fromUserId, - receivedVersion, - ); - - if (currentVersion != null) { - if (_requestedUpdates.contains(fromUserId)) { - // Only request a new version once per app session - return; - } - Log.info( - '[$receiptId] Having old version from contact. Requesting new version.', - ); - _requestedUpdates.add(fromUserId); - await sendCipherText( - fromUserId, - EncryptedContent( - userDiscoveryRequest: EncryptedContent_UserDiscoveryRequest( - currentVersion: currentVersion.toList(), - ), - ), - blocking: false, - ); - } -} - -Future handleUserDiscoveryRequest( - int fromUserId, - EncryptedContent_UserDiscoveryRequest request, - String receiptId, -) async { - Log.info('[$receiptId] Got a user discovery request'); - - if (!userService.currentUser.isUserDiscoveryEnabled) { - Log.warn('[$receiptId] Got a user discovery request while it is disabled'); - return; - } - final contact = await twonlyDB.contactsDao.getContactById(fromUserId); - - if (!UserDiscoveryService.isContactAllowed(contact)) { - Log.warn( - '[$receiptId] Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}', - ); - return; - } - - final newMessages = await UserDiscoveryService.getNewMessages( - fromUserId, - request.currentVersion, - ); - if (newMessages != null && newMessages.isNotEmpty) { - Log.info( - '[$receiptId] Sending ${newMessages.length} user discovery messages', - ); - await sendCipherText( - fromUserId, - EncryptedContent( - userDiscoveryUpdate: EncryptedContent_UserDiscoveryUpdate( - messages: newMessages, - ), - ), - blocking: false, - ); - } else { - Log.info( - '[$receiptId] Got update request, but there are no new updates for the user', - ); - } -} - -Future handleUserDiscoveryUpdate( - int fromUserId, - EncryptedContent_UserDiscoveryUpdate update, - String receiptId, -) async { - if (!userService.currentUser.isUserDiscoveryEnabled) { - Log.warn('[$receiptId] Got a user discovery update while it is disabled'); - return; - } - Log.info( - '[$receiptId] Got ${update.messages.length} user discovery messages', - ); - await UserDiscoveryService.handleNewMessages( - fromUserId, - update.messages.map(Uint8List.fromList).toList(), - ); -} diff --git a/lib/src/services/api/mediafiles/download.api.dart b/lib/src/services/api/mediafiles/download.api.dart index 57aa5327..eade098d 100644 --- a/lib/src/services/api/mediafiles/download.api.dart +++ b/lib/src/services/api/mediafiles/download.api.dart @@ -1,90 +1,28 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:background_downloader/background_downloader.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; -import 'package:cryptography_plus/cryptography_plus.dart'; -import 'package:drift/drift.dart'; -import 'package:http/http.dart' as http; -import 'package:mutex/mutex.dart'; -import 'package:path/path.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; -import 'package:twonly/src/utils/exclusive_access.utils.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; +/// Flutter only decides whether the user's network policy permits a download. +/// Transfer, validation, decryption, hashing, and state changes are Rust-owned. Future tryDownloadAllMediaFiles({bool force = false}) async { - // This is called when WebSocket is newly connected, so allow all downloads to be restarted. + if (force) { + await RustApi.downloadPendingMedia(); + return; + } final mediaFiles = await twonlyDB.mediaFilesDao .getAllMediaFilesPendingDownload(); - - for (final mediaFile in mediaFiles) { - if (await canMediaFileBeDownloaded(mediaFile)) { - await startDownloadMedia(mediaFile, force); + for (final media in mediaFiles) { + if (await isAllowedToDownload(media.type)) { + await RustApi.downloadMedia(mediaId: media.mediaId); } } } -Future canMediaFileBeDownloaded(MediaFile mediaFile) async { - final messages = await twonlyDB.messagesDao.getMessagesByMediaId( - mediaFile.mediaId, - ); - - // Verify that the sender of the original image / message does still exists. - // If not delete the message as it can not be downloaded from the server anymore. - - if (messages.length != 1) { - if (messages.isEmpty) { - MediaFileService(mediaFile).fullMediaRemoval(); - await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); - Log.warn( - 'Media file which is in downloading status has not text message. Deleting media file. ${mediaFile.mediaId}.', - ); - return false; - } - Log.warn( - 'A media for download must have one original message, but it has ${messages.length}.', - ); - return false; - } - - if (messages.first.senderId == null) { - Log.error('A media for download must have a sender id.'); - return false; - } - - final contact = await twonlyDB.contactsDao.getContactById( - messages.first.senderId!, - ); - - if (contact == null || contact.accountDeleted) { - Log.info( - 'Sender does not exists anymore. Delete media file and message.', - ); - await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); - await twonlyDB.messagesDao.deleteMessagesById(messages.first.messageId); - return false; - } - - return true; -} - -enum DownloadMediaTypes { - video, - image, - audio, -} +enum DownloadMediaTypes { video, image, audio } Map> defaultAutoDownloadOptions = { - ConnectivityResult.mobile.name: [ - DownloadMediaTypes.audio.name, - ], + ConnectivityResult.mobile.name: [DownloadMediaTypes.audio.name], ConnectivityResult.wifi.name: [ DownloadMediaTypes.video.name, DownloadMediaTypes.image.name, @@ -93,288 +31,40 @@ Map> defaultAutoDownloadOptions = { }; Future isAllowedToDownload(MediaType type) async { - if (type == MediaType.audio) { - return true; // always download audio files - } + if (type == MediaType.audio) return true; final connectivityResult = await Connectivity().checkConnectivity(); - final options = userService.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions; - if (connectivityResult.contains(ConnectivityResult.mobile)) { - if (type == MediaType.video) { - if (options[ConnectivityResult.mobile.name]!.contains( - DownloadMediaTypes.video.name, - )) { - return true; - } - } else if (options[ConnectivityResult.mobile.name]!.contains( - DownloadMediaTypes.image.name, - )) { - return true; - } + return options[ConnectivityResult.mobile.name]!.contains( + type == MediaType.video + ? DownloadMediaTypes.video.name + : DownloadMediaTypes.image.name, + ); } if (connectivityResult.contains(ConnectivityResult.wifi)) { - if (type == MediaType.video) { - if (options[ConnectivityResult.wifi.name]!.contains( - DownloadMediaTypes.video.name, - )) { - return true; - } - } else if (options[ConnectivityResult.wifi.name]!.contains( - DownloadMediaTypes.image.name, - )) { - return true; - } + return options[ConnectivityResult.wifi.name]!.contains( + type == MediaType.video + ? DownloadMediaTypes.video.name + : DownloadMediaTypes.image.name, + ); } return false; } -Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { - final mediaId = update.task.taskId.replaceAll('download_', ''); - var failed = false; - - if (update.status == TaskStatus.failed || - update.status == TaskStatus.canceled || - update.status == TaskStatus.notFound) { - failed = true; - } else if (update.status == TaskStatus.complete) { - if (update.responseStatusCode == 200) { - failed = false; - } else { - failed = true; - Log.warn( - '[$mediaId] Got invalid response status code: ${update.responseStatusCode}', - ); - Log.error( - 'Got invalid response status code: ${update.responseStatusCode}', - onlyIfSentryEnabled: true, - ); - } - } else { - Log.info('Got ${update.status} for $mediaId'); - return; - } - - if (failed) { - await requestMediaReupload(mediaId); - } else { - await handleEncryptedFile(mediaId); - } -} - -Mutex _protectDownload = Mutex(); -Mutex _protectDecryption = Mutex(); - Future startDownloadMedia(MediaFile media, bool force) async { - final mediaService = MediaFileService(media); - - if (mediaService.encryptedPath.existsSync()) { - await handleEncryptedFile(media.mediaId); - return; - } - - if (!force && !await isAllowedToDownload(media.type)) { - Log.warn( - 'Download blocked for ${media.mediaId} because of network state.', - ); - return; - } - - final isBlocked = await _protectDownload.protect(() async { - final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId); - - if (msg == null || msg.downloadState != DownloadState.pending) { - return true; - } - - await twonlyDB.mediaFilesDao.updateMedia( - msg.mediaId, - const MediaFilesCompanion( - downloadState: Value(DownloadState.downloading), - ), - ); - - return false; - }); - - if (isBlocked) { - Log.info('Download for ${media.mediaId} already started.'); - return; - } - - if (media.downloadToken == null) { - Log.info('Download token for ${media.mediaId} not found.'); - return; - } - - final downloadToken = uint8ListToHex(media.downloadToken!); - - final apiUrl = - 'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken'; - - try { - final task = DownloadTask( - url: apiUrl, - taskId: 'download_${media.mediaId}', - directory: mediaService.encryptedPath.parent.path, - baseDirectory: BaseDirectory.root, - filename: basename(mediaService.encryptedPath.path), - priority: 0, - retries: 10, - ); - - Log.info( - 'Downloading ${media.mediaId} to ${mediaService.encryptedPath}', - ); - - try { - await downloadFileFast(media, apiUrl, mediaService.encryptedPath); - } catch (e) { - Log.warn('Fast download failed: $e'); - await FileDownloader().enqueue(task); - } - } catch (e) { - Log.error('Exception during download: $e'); + if (force || await isAllowedToDownload(media.type)) { + await RustApi.downloadMedia(mediaId: media.mediaId); } } -Future downloadFileFast( - MediaFile media, - String apiUrl, - File filePath, -) async { - final response = await http - .get(Uri.parse(apiUrl)) - .timeout(const Duration(seconds: 30)); - - if (response.statusCode == 200) { - await filePath.writeAsBytes(response.bodyBytes); - Log.info('Fast Download successful: $filePath'); - await handleEncryptedFile(media.mediaId); - return; - } else { - if (response.statusCode == 404 || response.statusCode == 403) { - Log.warn( - '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... - await requestMediaReupload(media.mediaId); - return; - } - // Will be tried again using the slow method... - throw Exception('Fast download failed with status: ${response.statusCode}'); - } -} - -Future requestMediaReupload(String mediaId) async { - await twonlyDB.mediaFilesDao.updateMedia( - mediaId, - const MediaFilesCompanion( - downloadState: Value(DownloadState.reuploadRequested), - ), - ); - - final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); - - for (final message in messages) { - if (message.openedAt != null || message.senderId == null) continue; - await sendCipherText( - message.senderId!, - EncryptedContent( - mediaUpdate: EncryptedContent_MediaUpdate( - type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, - targetMessageId: message.messageId, - ), - ), - ); - } -} - -Future handleEncryptedFile(String mediaId) async { - await exclusiveAccess( - lockName: 'decryption-$mediaId', - mutex: _protectDecryption, - action: () async { - final mediaService = await MediaFileService.fromMediaId(mediaId); - if (mediaService == null) { - Log.warn('[$mediaId] Media file not found in database.'); - return; - } - - if (mediaService.mediaFile.downloadState == DownloadState.ready) { - Log.info('Decryption of $mediaId already finished.'); - return; - } - - if (!mediaService.encryptedPath.existsSync()) { - Log.warn( - 'Encrypted media file $mediaId does not exist anymore. Decryption probably already finished.', - ); - return; - } - - late Uint8List encryptedBytes; - try { - encryptedBytes = await mediaService.encryptedPath.readAsBytes(); - } catch (e) { - Log.error('Could not read encrypted media file: $e'); - await requestMediaReupload(mediaId); - return; - } - - try { - final chacha20 = FlutterChacha20.poly1305Aead(); - final secretKeyData = SecretKeyData( - mediaService.mediaFile.encryptionKey!, - ); - - final secretBox = SecretBox( - encryptedBytes, - nonce: mediaService.mediaFile.encryptionNonce!, - mac: Mac(mediaService.mediaFile.encryptionMac!), - ); - - final plaintextBytes = await chacha20.decrypt( - secretBox, - secretKey: secretKeyData, - ); - - final rawMediaBytes = Uint8List.fromList(plaintextBytes); - - await mediaService.tempPath.writeAsBytes(rawMediaBytes); - } catch (e) { - Log.error( - 'Could not decrypt the media file. Requesting a new upload.', - ); - await requestMediaReupload(mediaId); - return; - } - - await twonlyDB.mediaFilesDao.updateMedia( - mediaId, - const MediaFilesCompanion( - downloadState: Value(DownloadState.ready), - ), - ); - - await mediaService.hashMediaFile(); - - Log.info('Decryption of $mediaId was successful'); - - mediaService.encryptedPath.deleteSync(); - }, - ); -} +Future requestMediaReupload(String mediaId) => + RustApi.requestMediaReupload(mediaId: mediaId); Future makeMigrationToVersion91() async { - final messages = await twonlyDB.mediaFilesDao + final mediaFiles = await twonlyDB.mediaFilesDao .getAllMediaFilesReuploadRequested(); - for (final message in messages) { - await requestMediaReupload(message.mediaId); + for (final media in mediaFiles) { + await RustApi.requestMediaReupload(mediaId: media.mediaId); } } diff --git a/lib/src/services/api/mediafiles/media_background.api.dart b/lib/src/services/api/mediafiles/media_background.api.dart index 68f193f1..eedd83c2 100644 --- a/lib/src/services/api/mediafiles/media_background.api.dart +++ b/lib/src/services/api/mediafiles/media_background.api.dart @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/api/mediafiles/download.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; @@ -19,10 +18,6 @@ Future initFileDownloader() async { if (update.task.taskId.contains('upload_')) { await handleUploadStatusUpdate(update); } - if (update.task.taskId.contains('download_')) { - await handleDownloadStatusUpdate(update); - } - if (update.task.taskId.contains('backup_')) { await BackupService.handleBackupStatusUpdate( update.task.taskId, diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index 08c75586..8e5b688a 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -7,10 +7,6 @@ import 'package:twonly/src/database/twonly.db.dart' show Receipt; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; -// Compatibility adapters. All messaging state and behavior lives in Rust. -Future retransmitAllMessages() => - rust_api.RustApi.retransmitAllMessages(); - Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ String? receiptId, Receipt? receipt, diff --git a/lib/src/services/api/rust_api_result.dart b/lib/src/services/api/rust_api_result.dart new file mode 100644 index 00000000..67285516 --- /dev/null +++ b/lib/src/services/api/rust_api_result.dart @@ -0,0 +1,50 @@ +import 'dart:typed_data'; + +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/services/api/utils.api.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future> rustApiResult(Future request) async { + try { + return Result.success(await request); + } catch (error) { + final match = RegExp(r'API error code (\d+)').firstMatch(error.toString()); + if (match != null) { + return Result.error(ErrorCode.valueOf(int.parse(match.group(1)!))); + } + Log.error('Rust API call failed', error: error); + return Result.error(ErrorCode.InternalError); + } +} + +Future rustApiProtobuf( + Future request, + T Function(List) decode, +) async { + try { + return decode(await request); + } catch (error) { + Log.error('Rust API call failed', error: error); + return null; + } +} + +server.Response_UserData decodeUserData(List bytes) => + server.Response_UserData.fromBuffer(bytes); + +server.Response_ProofOfWork decodeProofOfWork(List bytes) => + server.Response_ProofOfWork.fromBuffer(bytes); + +server.Response_MemoriesUrl decodeMemoriesUrl(List bytes) => + server.Response_MemoriesUrl.fromBuffer(bytes); + +server.Response_MemoriesUploadUrls decodeMemoriesUploadUrls(List bytes) => + server.Response_MemoriesUploadUrls.fromBuffer(bytes); + +server.Response_MemoriesUsage decodeMemoriesUsage(List bytes) => + server.Response_MemoriesUsage.fromBuffer(bytes); + +server.Response_PlanBallance decodePlanBalance(List bytes) => + server.Response_PlanBallance.fromBuffer(bytes); diff --git a/lib/src/services/api/server_messages.api.dart b/lib/src/services/api/server_messages.api.dart deleted file mode 100644 index 712f28b0..00000000 --- a/lib/src/services/api/server_messages.api.dart +++ /dev/null @@ -1,748 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:clock/clock.dart'; -import 'package:drift/drift.dart'; -import 'package:hashlib/random.dart'; -import 'package:mutex/mutex.dart'; - -import 'package:twonly/globals.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/twonly.db.dart' hide Message; -import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' - as client; -import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.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.pbserver.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/client2client/additional_data.c2c.dart'; -import 'package:twonly/src/services/api/client2client/contact.c2c.dart'; -import 'package:twonly/src/services/api/client2client/errors.c2c.dart'; -import 'package:twonly/src/services/api/client2client/groups.c2c.dart'; -import 'package:twonly/src/services/api/client2client/media.c2c.dart'; -import 'package:twonly/src/services/api/client2client/messages.c2c.dart'; -import 'package:twonly/src/services/api/client2client/prekeys.c2c.dart'; -import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart'; -import 'package:twonly/src/services/api/client2client/reaction.c2c.dart'; -import 'package:twonly/src/services/api/client2client/text_message.c2c.dart'; -import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/group.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/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/session.signal.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; - -Future handleServerMessage(server.ServerToClient msg) async { - Log.info('Processing a message from the server.'); - - /// Returns means, that the server can delete the message from the server. - final ok = client.Response_Ok()..none = true; - var response = client.Response()..ok = ok; - - try { - if (msg.v0.hasRequestNewPreKeys()) { - response = await handleRequestNewPreKey(); - } else if (msg.v0.hasRequestNewPqcPreKeys()) { - response = (await handleRequestNewPqcPreKey()) ?? response; - } else if (msg.v0.hasNewMessage()) { - Log.info('Got 1 message from the server.'); - await handleClient2ClientMessage(msg.v0.newMessage); - } else if (msg.v0.hasNewMessages()) { - Log.info( - 'Got ${msg.v0.newMessages.newMessages.length} messages from the server.', - ); - final brokenSessionsInCurrentBatch = {}; - for (final newMessage in msg.v0.newMessages.newMessages) { - try { - await handleClient2ClientMessage( - newMessage, - brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch, - ); - } catch (e) { - Log.error(e); - } - } - } else { - Log.error('Unknown server message: $msg'); - } - } catch (e) { - Log.error(e); - } - - final v0 = client.V0() - ..seq = msg.v0.seq - ..response = response; - - final responseSent = await apiService.sendResponse(ClientToServer()..v0 = v0); - if (responseSent) { - Log.info( - 'Successfully queued response for server message ${msg.v0.seq}.', - ); - } else { - Log.warn('Could not send response for server message ${msg.v0.seq}.'); - } - AppState.gotMessageFromServer = true; - Log.info('All messages from the server processed.'); -} - -DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); - -final Map _messageLocks = {}; - -Future handleClient2ClientMessage( - NewMessage newMessage, { - Set? brokenSessionsInCurrentBatch, -}) async { - final body = Uint8List.fromList(newMessage.body); - final message = Message.fromBuffer(body); - final receiptId = message.receiptId; - - final mutex = _messageLocks.putIfAbsent(receiptId, Mutex.new); - if (mutex.isLocked) { - Log.info( - '[$receiptId] Skipping — already being processed by another handler', - ); - return; - } - await mutex.protect(() async { - try { - await _handleClient2ClientMessage( - newMessage, - message, - brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch, - ); - } finally { - _messageLocks.remove(receiptId); - } - }); -} - -Future _handleClient2ClientMessage( - NewMessage newMessage, - Message message, { - Set? brokenSessionsInCurrentBatch, -}) async { - final fromUserId = newMessage.fromUserId.toInt(); - 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 (message.type == Message_Type.SENDER_DELIVERY_RECEIPT) { - Log.info( - '[$receiptId] Delivery receipt is a duplicate. Skipping receipt response.', - ); - return; - } - const duplicateReceiptCooldown = Duration(days: 10); - final shouldResend = await twonlyDB.receiptsDao.claimDuplicateReceiptResend( - receiptId, - duplicateReceiptCooldown, - ); - if (!shouldResend) { - Log.info( - '[$receiptId] Message is a duplicate. Skipping delivery receipt during cooldown.', - ); - return; - } - Log.info( - '[$receiptId] Message is a duplicate and cooldown elapsed. Sending delivery receipt again.', - ); - try { - final response = Message(type: Message_Type.SENDER_DELIVERY_RECEIPT); - await twonlyDB.receiptsDao.insertReceipt( - ReceiptsCompanion( - receiptId: Value(receiptId), - contactId: Value(fromUserId), - message: Value(response.writeToBuffer()), - contactWillSendsReceipt: const Value(false), - ), - ); - } catch (e) { - Log.warn('[$receiptId] Error handling duplicate receipt ACK: $e'); - } - await tryToSendCompleteMessage( - receiptId: receiptId, - blocking: false, - ); - return; - } - - Log.info('[$receiptId] Started processing message'); - - switch (message.type) { - case Message_Type.SENDER_DELIVERY_RECEIPT: - Log.info('[$receiptId] Got delivery receipt!'); - await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId); - - case Message_Type.PLAINTEXT_CONTENT: - var retry = false; - if (message.hasPlaintextContent()) { - if (message.plaintextContent.hasDecryptionErrorMessage()) { - if (message.plaintextContent.decryptionErrorMessage.type == - PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN) { - // Get a new prekey from the server, and establish a new signal session. - await handleSessionResync(fromUserId); - } - Log.info( - '[$receiptId] Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type}', - ); - retry = true; - } - if (message.plaintextContent.hasRetryControlError()) { - Log.info( - '[$receiptId] Got access control error. Resending message.', - ); - retry = true; - } - } - - if (retry) { - final newReceiptId = uuid.v4(); - await twonlyDB.receiptsDao.updateReceipt( - receiptId, - ReceiptsCompanion( - receiptId: Value(newReceiptId), - ackByServerAt: const Value(null), - ), - ); - Log.info( - '[$receiptId] Sending error message to the original sender with receiptId $newReceiptId.', - ); - await tryToSendCompleteMessage( - receiptId: newReceiptId, - blocking: false, - ); - } - - case Message_Type.CIPHERTEXT: - case Message_Type.CIPHERTEXT_V2: - case Message_Type.PREKEY_BUNDLE: - if (message.hasEncryptedContent()) { - Value? receiptIdDB; - - final encryptedContentRaw = Uint8List.fromList( - message.encryptedContent, - ); - - Message? response; - - final user = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - - if (user == null) { - if (!await addNewHiddenContact(fromUserId)) { - // in case the user could not be added, send a retry error message as this error should only happen in case - // it was not possible to load the user from the server - response = Message( - receiptId: receiptId, - type: Message_Type.PLAINTEXT_CONTENT, - plaintextContent: PlaintextContent( - retryControlError: PlaintextContent_RetryErrorMessage(), - ), - ); - } - } - - if (response == null) { - final ( - encryptedContent, - plainTextContent, - ) = await handleEncryptedMessageRaw( - fromUserId, - encryptedContentRaw, - message.type, - receiptId, - brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch, - ); - if (plainTextContent != null) { - response = Message( - receiptId: receiptId, - type: Message_Type.PLAINTEXT_CONTENT, - plaintextContent: plainTextContent, - ); - } else if (encryptedContent != null) { - response = Message( - type: Message_Type.CIPHERTEXT, - 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(); - } else { - // Message was successful processed - } - } - - response ??= Message(type: Message_Type.SENDER_DELIVERY_RECEIPT); - - String? targetReceiptId; - try { - final inserted = await twonlyDB.receiptsDao.insertReceipt( - ReceiptsCompanion( - receiptId: receiptIdDB ?? Value(receiptId), - contactId: Value(fromUserId), - message: Value(response.writeToBuffer()), - 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) { - Log.warn('[$receiptId] Error inserting receipt: $e'); - } - - targetReceiptId ??= receiptIdDB?.value ?? receiptId; - - await tryToSendCompleteMessage( - receiptId: targetReceiptId, - blocking: false, - ); - } - case Message_Type.TEST_NOTIFICATION: - break; - } - - try { - await twonlyDB.receiptsDao.gotReceipt(receiptId); - Log.info('[$receiptId] Finished processing'); - } catch (e) { - Log.warn('[$receiptId] Error marking message as received: $e'); - Log.error( - 'Error marking message as received: $e', - onlyIfSentryEnabled: true, - ); - } -} - -Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw( - int fromUserId, - Uint8List encryptedContentRaw, - Message_Type messageType, - String receiptId, { - Set? brokenSessionsInCurrentBatch, -}) async { - Log.info('[$receiptId] calling signalDecryptMessage'); - EncryptedContent? encryptedContent; - PlaintextContent_DecryptionErrorMessage_Type? decryptionErrorType; - - if (messageType == Message_Type.CIPHERTEXT_V2) { - (encryptedContent, decryptionErrorType) = await signalDecryptMessageV2( - fromUserId, - encryptedContentRaw, - brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch, - ); - } else { - (encryptedContent, decryptionErrorType) = await signalDecryptMessageV1( - fromUserId, - encryptedContentRaw, - messageType.value, - brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch, - ); - } - - if (encryptedContent == null) { - return ( - null, - PlaintextContent( - decryptionErrorMessage: PlaintextContent_DecryptionErrorMessage( - type: decryptionErrorType ??= - PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, - ), - ), - ); - } - - Log.info('[$receiptId] Calling handleEncryptedMessage'); - - final result = await handleEncryptedMessage( - fromUserId, - encryptedContent, - messageType, - receiptId, - ); - - Log.info('[$receiptId] Finished handleEncryptedMessage'); - - if (result.responseCipherText == null && result.responsePlaintext == null) { - unawaited(FcmNotificationService.updateLastServerMessageTimestamp()); - if (Platform.isAndroid && result.showPushNotification) { - // Message was handled without any error. Show push notification to the user for Android. - await showPushNotificationFromServerMessages( - fromUserId, - encryptedContent, - ); - } - } - - return (result.responseCipherText, result.responsePlaintext); -} - -Future handleEncryptedMessage( - int fromUserId, - EncryptedContent content, - Message_Type messageType, - String receiptId, -) async { - // We got a valid message fromUserId, so mark all messages which where - // send to the user but not yet ACK for retransmission. All marked messages - // will be either transmitted again after a new server connection (minimum 20 seconds). - // In case the server sends the ACK before they will be deleted. - // This ensures that 1. all messages will be received by the other person and - // that they will be retransmitted in case the server deleted them as they - // where not downloaded within the 40 days - await twonlyDB.receiptsDao.markMessagesForRetry(fromUserId); - - final senderProfileCounter = await checkForProfileUpdate(fromUserId, content); - if (userService.currentUser.isUserDiscoveryEnabled && - content.hasSenderUserDiscoveryVersion()) { - await checkForUserDiscoveryChanges( - fromUserId, - content.senderUserDiscoveryVersion, - receiptId, - ); - } - - 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 (!await handleContactRequest( - fromUserId, - content.contactRequest, - receiptId, - )) { - return DecryptedMessageResult( - responsePlaintext: PlaintextContent() - ..retryControlError = PlaintextContent_RetryErrorMessage(), - ); - } - return const DecryptedMessageResult(); - } - - if (content.hasErrorMessages()) { - await handleErrorMessage( - fromUserId, - content.errorMessages, - receiptId, - groupId: content.hasGroupId() ? content.groupId : 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()) { - await handleContactUpdate( - fromUserId, - content.contactUpdate, - senderProfileCounter, - receiptId, - ); - return const DecryptedMessageResult(showPushNotification: false); - } - - if (content.hasUserDiscoveryRequest()) { - await handleUserDiscoveryRequest( - fromUserId, - content.userDiscoveryRequest, - receiptId, - ); - return const DecryptedMessageResult(showPushNotification: false); - } - - if (content.hasUserDiscoveryUpdate()) { - await handleUserDiscoveryUpdate( - fromUserId, - content.userDiscoveryUpdate, - receiptId, - ); - return const DecryptedMessageResult(); - } - - if (content.hasPushKeys()) { - await handlePushKey(fromUserId, content.pushKeys, receiptId); - return const DecryptedMessageResult(); - } - - if (content.hasMessageUpdate()) { - await handleMessageUpdate( - fromUserId, - content.messageUpdate, - receiptId, - ); - return const DecryptedMessageResult(); - } - - if (content.hasKeyVerificationProof()) { - await KeyVerificationService.handleVerificationProof( - fromUserId, - content.keyVerificationProof.calculatedMac, - ); - return const DecryptedMessageResult(); - } - - if (content.hasMediaUpdate()) { - await handleMediaUpdate( - fromUserId, - content.mediaUpdate, - receiptId, - ); - return const DecryptedMessageResult(); - } - - if (!content.hasGroupId()) { - final type = _getEncryptedContentType(content); - 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()) { - await handleGroupCreate( - fromUserId, - content.groupId, - content.groupCreate, - receiptId, - ); - return const DecryptedMessageResult(); - } - - /// Verify that the user is (still) in that group... - if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) { - // Check if this is a direct chat... - if (getUUIDforDirectChat(userService.currentUser.userId, fromUserId) == - content.groupId) { - final contact = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - Log.info( - '[$receiptId] Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})', - ); - if (contact == null || !contact.accepted || contact.deletedByUser) { - await handleNewContactRequest(fromUserId); - Log.warn( - '[$receiptId] User tries to send message to direct chat while the user does not exist!', - ); - return DecryptedMessageResult( - responseCipherText: EncryptedContent( - errorMessages: EncryptedContent_ErrorMessages( - type: EncryptedContent_ErrorMessages_Type - .ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD, - relatedReceiptId: receiptId, - ), - ), - ); - } - Log.info( - '[$receiptId] Creating new DirectChat between two users', - ); - await twonlyDB.groupsDao.createNewDirectChat( - fromUserId, - GroupsCompanion( - groupName: Value(getContactDisplayName(contact)), - ), - ); - } else { - if (content.hasGroupJoin()) { - Log.warn( - '[$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. - return DecryptedMessageResult( - responsePlaintext: PlaintextContent() - ..retryControlError = PlaintextContent_RetryErrorMessage(), - ); - } - - Log.warn( - '[$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, - ), - ), - ); - } - } - - if (content.hasFlameSync()) { - await handleFlameSync(content.groupId, content.flameSync, receiptId); - return const DecryptedMessageResult(); - } - - if (content.hasGroupUpdate()) { - await handleGroupUpdate( - fromUserId, - content.groupId, - content.groupUpdate, - receiptId, - ); - return const DecryptedMessageResult(); - } - - if (content.hasGroupJoin()) { - if (!await handleGroupJoin( - fromUserId, - content.groupId, - content.groupJoin, - receiptId, - )) { - return DecryptedMessageResult( - responsePlaintext: PlaintextContent() - ..retryControlError = PlaintextContent_RetryErrorMessage(), - ); - } - return const DecryptedMessageResult(); - } - - if (content.hasResendGroupPublicKey()) { - await handleResendGroupPublicKey( - fromUserId, - content.groupId, - content.groupJoin, - receiptId, - ); - return const DecryptedMessageResult(); - } - - if (content.hasAdditionalDataMessage()) { - await handleAdditionalDataMessage( - fromUserId, - content.groupId, - content.additionalDataMessage, - receiptId, - ); - return const DecryptedMessageResult(); - } - - if (content.hasTextMessage()) { - final isNewText = await handleTextMessage( - fromUserId, - content.groupId, - content.textMessage, - receiptId, - ); - return DecryptedMessageResult(showPushNotification: isNewText); - } - - if (content.hasReaction()) { - await handleReaction( - fromUserId, - content.groupId, - content.reaction, - receiptId, - ); - return const DecryptedMessageResult(); - } - - if (content.hasMedia()) { - final isNewMedia = await handleMedia( - fromUserId, - content.groupId, - content.media, - receiptId, - ); - return DecryptedMessageResult(showPushNotification: isNewMedia); - } - - if (content.hasTypingIndicator()) { - await handleTypingIndicator( - fromUserId, - content.groupId, - content.typingIndicator, - receiptId, - ); - } - - 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; -} diff --git a/lib/src/services/api/utils.api.dart b/lib/src/services/api/utils.api.dart index b51906cc..d84cf8c2 100644 --- a/lib/src/services/api/utils.api.dart +++ b/lib/src/services/api/utils.api.dart @@ -6,30 +6,26 @@ import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart' - as client; -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/server_to_client.pb.dart' as server; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' hide Message; import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/secure_storage.dart'; class Result { - Result.error(this.error) : value = null; - Result.success(this.value) : error = null; + Result.error(this.error) : value = null, _isSuccess = false; + Result.success(this.value) : error = null, _isSuccess = true; final T? value; final E? error; + final bool _isSuccess; - bool get isSuccess => value != null; - bool get isError => error != null; + bool get isSuccess => _isSuccess; + bool get isError => !_isSuccess; } DateTime fromTimestamp(Int64 timeStamp) { @@ -53,22 +49,6 @@ Result asResult(server.ServerToClient? msg) { } } -ClientToServer createClientToServerFromHandshake(Handshake handshake) { - final v0 = client.V0() - ..seq = Int64() - ..handshake = handshake; - return ClientToServer()..v0 = v0; -} - -ClientToServer createClientToServerFromApplicationData( - ApplicationData applicationData, -) { - final v0 = client.V0() - ..seq = Int64() - ..applicationdata = applicationData; - return ClientToServer()..v0 = v0; -} - Future handleMediaError(MediaFile media) async { await twonlyDB.mediaFilesDao.updateMedia( media.mediaId, @@ -100,11 +80,6 @@ Future importSignalContactAndCreateRequest( return false; } - // 1. Setup notifications keys with the other user - await setupNotificationWithUsers( - forceContact: userdata.userId.toInt(), - ); - // 2. Then send user request await sendCipherText( userdata.userId.toInt(), diff --git a/lib/src/services/backup.service.dart b/lib/src/services/backup.service.dart index 242166b2..f122ae99 100644 --- a/lib/src/services/backup.service.dart +++ b/lib/src/services/backup.service.dart @@ -309,8 +309,11 @@ class BackupService { String username, String password, ) async { - final userId = await apiService.getUserIdFromUsername(username); - if (userId == null) { + late final int userId; + try { + userId = await RustApi.getUserIdFromUsername(username: username); + } catch (error) { + Log.error('Could not resolve backup username', error: error); return RecoveryError.usernameNotValid; } diff --git a/lib/src/services/memories/memories_cloud.service.dart b/lib/src/services/memories/memories_cloud.service.dart index b90cb86b..97a63aed 100644 --- a/lib/src/services/memories/memories_cloud.service.dart +++ b/lib/src/services/memories/memories_cloud.service.dart @@ -171,7 +171,10 @@ class MemoriesCloudService { return true; } - final urls = await apiService.getMemoriesUrl(mediaId, isThumbnail); + final urls = await rustApiProtobuf( + RustApi.getMemoriesUrl(mediaId: mediaId, thumbnail: isThumbnail), + decodeMemoriesUrl, + ); if (urls == null || !urls.hasFullDownloadUrl()) return false; try { @@ -205,10 +208,13 @@ class MemoriesCloudService { } final sizeBytes = ms.storedPath.lengthSync(); - final urls = await apiService.requestMemoriesUpload( - sizeBytes, - mediaFile.createdAt, - mediaFile.mediaId, + final urls = await rustApiProtobuf( + RustApi.requestMemoriesUpload( + size: sizeBytes, + originalDate: mediaFile.createdAt.millisecondsSinceEpoch, + mediaId: mediaFile.mediaId, + ), + decodeMemoriesUploadUrls, ); if (urls == null) { @@ -268,8 +274,8 @@ class MemoriesCloudService { } // 3. Confirm upload - final confirmRes = await apiService.confirmMemoriesUpload( - mediaFile.mediaId, + final confirmRes = await rustApiResult( + RustApi.confirmMemoriesUpload(mediaId: mediaFile.mediaId), ); if (confirmRes.isSuccess) { await twonlyDB.mediaFilesDao.updateMedia( diff --git a/lib/src/services/migrations.service.dart b/lib/src/services/migrations.service.dart index d8530cf3..d2852d76 100644 --- a/lib/src/services/migrations.service.dart +++ b/lib/src/services/migrations.service.dart @@ -98,10 +98,8 @@ Future runMigrations() async { ..canUseLoginTokenForAuth = false // As usernames changes where not considered in the old version force users // to reenter there passwords. - // ignore: deprecated_member_use_from_same_package - ..twonlySafeBackup?.encryptionKey = [] - // ignore: deprecated_member_use_from_same_package - ..twonlySafeBackup?.backupId = []; + ..twonlySafeBackup?.encryptionKey = Uint8List(0) + ..twonlySafeBackup?.backupId = Uint8List(0); }); } } diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index df1d9fa6..63d607cc 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -9,14 +9,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.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_de.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/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/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -50,29 +47,6 @@ Future customLocalPushNotification(String title, String msg) async { ); } -Future showPushNotificationFromServerMessages( - int fromUserId, - EncryptedContent encryptedContent, -) async { - final pushData = await getPushNotificationFromEncryptedContent( - null, // this is the toUserID which must be null as this means that the targetMessageId was send from this user. - null, - encryptedContent, - ); - if (pushData != null) { - final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); - for (final pushUser in pushUsers) { - if (pushUser.userId.toInt() == fromUserId) { - String? groupId; - if (encryptedContent.hasGroupId()) { - groupId = encryptedContent.groupId; - } - return showLocalPushNotification(pushUser, pushData, groupId: groupId); - } - } - } -} - Future tryDecryptMessage( List key, EncryptedPushNotification push, diff --git a/lib/src/services/notifications/fcm.notifications.dart b/lib/src/services/notifications/fcm.notifications.dart index 27ba1505..9323408a 100644 --- a/lib/src/services/notifications/fcm.notifications.dart +++ b/lib/src/services/notifications/fcm.notifications.dart @@ -36,22 +36,16 @@ class FcmNotificationService { static Future initFCMAfterAuthenticated({bool force = false}) async { final fcmToken = userService.currentUser.fcmToken; - if (userService.currentUser.updateFCMToken || force) { + if (userService.currentUser.updateFcmToken || force) { if (fcmToken == null) { Log.error('FCM token could not be updated as it is empty'); await _checkForTokenUpdates(); return; } - final res = await apiService.updateFCMToken( - fcmToken, - ); - if (res.isSuccess) { - Log.info('Uploaded new FCM token!'); + if (await _uploadFcmToken(fcmToken)) { await UserService.update((u) { - u.updateFCMToken = false; + u.updateFcmToken = false; }); - } else { - Log.error('Could not update FCM token!'); } } } @@ -100,18 +94,14 @@ class FcmNotificationService { Log.info('Got new FCM token.'); await UserService.update((u) { u - ..updateFCMToken = true + ..updateFcmToken = true ..fcmToken = fcmToken; }); if (apiService.isAuthenticated) { - final res = await apiService.updateFCMToken(fcmToken); - if (res.isSuccess) { - Log.info('Uploaded new FCM token!'); + if (await _uploadFcmToken(fcmToken)) { await UserService.update((u) { - u.updateFCMToken = false; + u.updateFcmToken = false; }); - } else { - Log.error('Could not update FCM token!'); } } } @@ -121,18 +111,14 @@ class FcmNotificationService { .listen((String fcmToken) async { await UserService.update((u) { u - ..updateFCMToken = true + ..updateFcmToken = true ..fcmToken = fcmToken; }); if (apiService.isAuthenticated) { - final res = await apiService.updateFCMToken(fcmToken); - if (res.isSuccess) { - Log.info('Uploaded new FCM token!'); + if (await _uploadFcmToken(fcmToken)) { await UserService.update((u) { - u.updateFCMToken = false; + u.updateFcmToken = false; }); - } else { - Log.error('Could not update FCM token!'); } } }) @@ -144,6 +130,17 @@ class FcmNotificationService { } } + static Future _uploadFcmToken(String token) async { + try { + await RustApi.updateFcmToken(token: token); + Log.info('Uploaded new FCM token!'); + return true; + } catch (error) { + Log.error('Could not update FCM token!', error: error); + return false; + } + } + static Future handleRemoteMessage(RemoteMessage message) async { Log.info('handleRemoteMessage received message: ${message.messageId}'); await _updateLastFcmMessageTimestamp(); diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart deleted file mode 100644 index e802f0ac..00000000 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ /dev/null @@ -1,446 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; - -import 'package:clock/clock.dart'; -import 'package:collection/collection.dart'; -import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; -import 'package:cryptography_plus/cryptography_plus.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:hashlib/random.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; -import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/mediafiles.table.dart'; -import 'package:twonly/src/database/twonly.db.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/services/api/messages.api.dart'; -import 'package:twonly/src/utils/log.dart'; -import 'package:twonly/src/utils/misc.dart'; - -/// This function must be called after the database is setup -Future setupNotificationWithUsers({ - bool force = false, - int? forceContact, -}) async { - var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); - - // HotFIX: Search for user with id 0 if not there remove all - // and create new push keys with all users. - final pushUser = pushUsers.firstWhereOrNull((x) => x.userId.toInt() == 0); - if (pushUser == null) { - Log.info('Clearing push keys'); - await setPushKeys(SecureStorageKeys.receivingPushKeys, []); - pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys) - ..add( - PushUser( - userId: Int64(), - displayName: 'NoUser', - pushKeys: [], - ), - ); - } - - var wasChanged = false; - - final random = Random.secure(); - - final contacts = await twonlyDB.contactsDao.getAllContacts(); - for (final contact in contacts) { - final pushUser = pushUsers.firstWhereOrNull( - (x) => x.userId.toInt() == contact.userId, - ); - - if (pushUser != null && pushUser.pushKeys.isNotEmpty) { - // make it harder to predict the change of the key - final timeBefore = clock.now().subtract( - Duration(days: 10 + random.nextInt(5)), - ); - final lastKey = pushUser.pushKeys.last; - final createdAt = DateTime.fromMillisecondsSinceEpoch( - lastKey.createdAtUnixTimestamp.toInt(), - ); - - if (force || - (forceContact == contact.userId) || - createdAt.isBefore(timeBefore)) { - final pushKey = PushKey( - id: lastKey.id + random.nextInt(5), - key: List.generate(32, (index) => random.nextInt(256)), - createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch), - ); - await sendNewPushKey(contact.userId, pushKey); - // only store a maximum of two keys - pushUser.pushKeys.clear(); - pushUser.pushKeys.add(lastKey); - pushUser.pushKeys.add(pushKey); - wasChanged = true; - Log.info('Creating new pushkey for ${contact.userId}'); - } - } else { - Log.info( - 'User ${contact.userId} not yet in pushkeys. Creating a new user.', - ); - wasChanged = true; - - /// Insert a new push user - final pushKey = PushKey( - id: Int64(1), - key: List.generate(32, (index) => random.nextInt(256)), - createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch), - ); - await sendNewPushKey(contact.userId, pushKey); - pushUsers.add( - PushUser( - userId: Int64(contact.userId), - displayName: getContactDisplayName(contact), - blocked: contact.blocked, - pushKeys: [pushKey], - ), - ); - } - } - - if (wasChanged) { - await setPushKeys(SecureStorageKeys.receivingPushKeys, pushUsers); - } -} - -Future sendNewPushKey(int userId, PushKey pushKey) async { - await sendCipherText( - userId, - EncryptedContent() - ..pushKeys = (EncryptedContent_PushKeys() - ..type = EncryptedContent_PushKeys_Type.UPDATE - ..key = pushKey.key - ..keyId = pushKey.id - ..createdAt = pushKey.createdAtUnixTimestamp), - ); -} - -Future updatePushUser(Contact contact) async { - final pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys); - - final pushUser = pushKeys.firstWhereOrNull( - (x) => x.userId.toInt() == contact.userId, - ); - - if (pushUser == null) { - pushKeys.add( - PushUser( - userId: Int64(contact.userId), - displayName: getContactDisplayName(contact), - pushKeys: [], - blocked: contact.blocked, - lastMessageId: uuid.v7(), - ), - ); - } else { - pushUser - ..displayName = getContactDisplayName(contact) - ..blocked = contact.blocked; - } - - await setPushKeys(SecureStorageKeys.receivingPushKeys, pushKeys); -} - -Future handleNewPushKey(int fromUserId, int keyId, List key) async { - final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys); - - var pushUser = pushKeys.firstWhereOrNull( - (x) => x.userId.toInt() == fromUserId, - ); - - if (pushUser == null) { - final contact = await twonlyDB.contactsDao - .getContactByUserId(fromUserId) - .getSingleOrNull(); - if (contact == null) return; - pushKeys.add( - PushUser( - userId: Int64(fromUserId), - displayName: getContactDisplayName(contact), - pushKeys: [], - blocked: contact.blocked, - lastMessageId: uuid.v7(), - ), - ); - pushUser = pushKeys.firstWhereOrNull((x) => x.userId.toInt() == fromUserId); - } - - if (pushUser == null) { - Log.error('could not store new push key as no user was found'); - } - - // only store the newest key... - pushUser!.pushKeys.clear(); - pushUser.pushKeys.add( - PushKey( - id: Int64(keyId), - key: key, - createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch), - ), - ); - - await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys); -} - -Future updateLastMessageId(int fromUserId, String messageId) async { - final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys); - - final pushUser = pushUsers.firstWhereOrNull( - (x) => x.userId.toInt() == fromUserId, - ); - if (pushUser == null) { - unawaited(setupNotificationWithUsers()); - return; - } - - if (isUUIDNewer(messageId, pushUser.lastMessageId)) { - pushUser.lastMessageId = messageId; - await setPushKeys(SecureStorageKeys.receivingPushKeys, pushUsers); - } -} - -Future getPushNotificationFromEncryptedContent( - int? toUserId, - String? messageId, - EncryptedContent content, -) async { - PushKind? kind; - String? additionalContent; - - if (content.hasReaction()) { - if (content.reaction.remove) return null; - - final msg = await twonlyDB.messagesDao - .getMessageById(content.reaction.targetMessageId) - .getSingleOrNull(); - if (msg == null || msg.senderId != toUserId) { - return null; - } - if (msg.content != null) { - kind = PushKind.REACTION_TO_TEXT; - } else if (msg.mediaId != null) { - final media = await twonlyDB.mediaFilesDao.getMediaFileById(msg.mediaId!); - if (media == null) return null; - switch (media.type) { - case MediaType.image: - kind = PushKind.REACTION_TO_IMAGE; - case MediaType.audio: - kind = PushKind.REACTION_TO_AUDIO; - case MediaType.video: - kind = PushKind.REACTION_TO_VIDEO; - case MediaType.gif: - kind = PushKind.REACTION; - } - } - additionalContent = content.reaction.emoji; - } - - if (content.hasTextMessage()) { - kind = PushKind.TEXT; - if (content.textMessage.hasQuoteMessageId()) { - kind = PushKind.RESPONSE; - } - final group = await twonlyDB.groupsDao.getGroup(content.groupId); - if (group != null && !group.isDirectChat) { - additionalContent = group.groupName; - } - } - - if (content.hasAdditionalDataMessage()) { - kind = PushKind.TEXT; - } - - if (content.hasMedia()) { - switch (content.media.type) { - case EncryptedContent_Media_Type.REUPLOAD: - return null; - case EncryptedContent_Media_Type.IMAGE: - kind = PushKind.IMAGE; - case EncryptedContent_Media_Type.VIDEO: - kind = PushKind.VIDEO; - case EncryptedContent_Media_Type.GIF: - kind = PushKind.IMAGE; - case EncryptedContent_Media_Type.AUDIO: - kind = PushKind.AUDIO; - } - if (content.media.requiresAuthentication) { - kind = PushKind.TWONLY; - } - final group = await twonlyDB.groupsDao.getGroup(content.groupId); - if (group != null && !group.isDirectChat) { - additionalContent = group.groupName; - } - } - - if (content.hasContactRequest()) { - switch (content.contactRequest.type) { - case EncryptedContent_ContactRequest_Type.REQUEST: - kind = PushKind.CONTACT_REQUEST; - case EncryptedContent_ContactRequest_Type.ACCEPT: - kind = PushKind.ACCEPT_REQUEST; - case EncryptedContent_ContactRequest_Type.REJECT: - return null; - } - } - - if (content.hasMediaUpdate()) { - final msg = await twonlyDB.messagesDao - .getMessageById(content.mediaUpdate.targetMessageId) - .getSingleOrNull(); - // These notifications should only be send to the original sender. - if (msg == null || msg.senderId != toUserId) { - return null; - } - switch (content.mediaUpdate.type) { - case EncryptedContent_MediaUpdate_Type.REOPENED: - kind = PushKind.REOPENED_MEDIA; - case EncryptedContent_MediaUpdate_Type.STORED: - kind = PushKind.STORED_MEDIA_FILE; - case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR: - return null; - } - } - - if (content.hasGroupCreate()) { - kind = PushKind.ADDED_TO_GROUP; - final group = await twonlyDB.groupsDao.getGroup(content.groupId); - if (group != null) { - additionalContent = group.groupName; - } - } - - if (kind == null) return null; - - final pushNotification = PushNotification()..kind = kind; - if (additionalContent != null) { - pushNotification.additionalContent = additionalContent; - } - if (messageId != null) { - pushNotification.messageId = messageId; - } - return pushNotification; -} - -Future requestNewPushKeysForUser(int toUserId) async { - await sendCipherText( - toUserId, - EncryptedContent() - ..pushKeys = (EncryptedContent_PushKeys() - ..type = EncryptedContent_PushKeys_Type.REQUEST), - ); -} - -/// this will trigger a push notification -/// push notification only containing the message kind and username -Future encryptPushNotification( - int toUserId, - PushNotification content, -) async { - final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys); - - var key = 'InsecureOnlyUsedForAddingContact'.codeUnits; - var keyId = 0; - - final pushUser = pushKeys.firstWhereOrNull( - (x) => x.userId.toInt() == toUserId, - ); - - if (pushUser == null) { - // user does not have send any push keys - // only allow accept request and contact request to be send in an insecure way :/ - // In future find a better way, e.g. use the signal protocol in a native way.. - if (content.kind != PushKind.ACCEPT_REQUEST && - content.kind != PushKind.CONTACT_REQUEST && - content.kind != PushKind.TEST_NOTIFICATION) { - // this will be enforced after every app uses this system... :/ - // return null; - Log.warn('Using insecure key as the receiver does not send a push key!'); - await requestNewPushKeysForUser(toUserId); - } - } else { - final createdAt = DateTime.fromMillisecondsSinceEpoch( - pushUser.pushKeys.last.createdAtUnixTimestamp.toInt(), - ); - final timeBefore = clock.now().subtract(const Duration(days: 8)); - if (createdAt.isBefore(timeBefore)) { - await requestNewPushKeysForUser(toUserId); - } - try { - key = pushUser.pushKeys.last.key; - keyId = pushUser.pushKeys.last.id.toInt(); - } catch (e) { - Log.error('No push notification key found for user $toUserId'); - return null; - } - } - - final chacha20 = FlutterChacha20.poly1305Aead(); - final nonce = chacha20.newNonce(); - final secretBox = await chacha20.encrypt( - content.writeToBuffer(), - secretKey: SecretKeyData(key), - nonce: nonce, - ); - final res = EncryptedPushNotification( - keyId: Int64(keyId), - nonce: nonce, - ciphertext: secretBox.cipherText, - mac: secretBox.mac.bytes, - ); - return res.writeToBuffer(); -} - -Future> getPushKeys(String storageKey) async { - const storage = FlutterSecureStorage(); - try { - final pushKeysProto = await storage.read( - key: storageKey, - iOptions: const IOSOptions( - groupId: 'CN332ZUGRP.eu.twonly.shared', - accessibility: KeychainAccessibility.first_unlock, - ), - ); - if (pushKeysProto == null) return []; - final pushKeysRaw = base64Decode(pushKeysProto); - return PushUsers.fromBuffer(pushKeysRaw).users; - } catch (e) { - Log.error(e); - } - return []; -} - -Future setPushKeys(String storageKey, List pushKeys) async { - const storage = FlutterSecureStorage(); - - try { - await storage.delete( - key: storageKey, - iOptions: const IOSOptions( - groupId: 'CN332ZUGRP.eu.twonly.shared', - accessibility: KeychainAccessibility.first_unlock, - ), - ); - } catch (e) { - Log.error(e); - } - - final jsonString = base64Encode(PushUsers(users: pushKeys).writeToBuffer()); - try { - await storage.write( - key: storageKey, - value: jsonString, - iOptions: const IOSOptions( - groupId: 'CN332ZUGRP.eu.twonly.shared', - accessibility: KeychainAccessibility.first_unlock, - ), - ); - } catch (e) { - Log.error(e); - } -} diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index 46fafde8..242eb503 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -1,23 +1,23 @@ 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 Hkdf, Hmac, Mac, SecretBox, SecretKey, Xchacha20; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' + show Int64List; import 'package:twonly/core/bridge/wrapper.dart'; import 'package:twonly/core/bridge/wrapper/key_manager.dart'; +import 'package:twonly/core/user_config.dart' show PasswordlessRecoveryConfig; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/keyvalue.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/api/websocket/server_to_client.pb.dart' + as server; 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'; @@ -128,11 +128,11 @@ class PasswordlessRecoveryService { mac: secretBox.mac.bytes, ); - final res = await apiService.submitRecoveryShare( + await RustApi.submitRecoveryShare( notificationId: notificationId, encryptedMessage: envelope.writeToBuffer(), ); - return res.isSuccess; + return true; } catch (e) { Log.error('Failed to submit recovery share', error: e); return false; @@ -172,7 +172,7 @@ class PasswordlessRecoveryService { await twonlyDB.contactsDao.resetRecoveryDataForAllContacts(); await UserService.update((u) => u.passwordLessRecovery = null); - final config = PasswordLessRecovery(threshold); + final config = PasswordlessRecoveryConfig(threshold: threshold); final xchacha20 = Xchacha20.poly1305Aead(); // 2. If enabled, handle the second factor and create serverKey @@ -309,7 +309,7 @@ class PasswordlessRecoveryService { ); } - unawaited(performHeartbeat()); + unawaited(RustApi.performPasswordlessRecoveryHeartbeat()); // The passwordless is configured successfully. return true; @@ -357,193 +357,6 @@ class PasswordlessRecoveryService { } } - static Future 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 contact 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 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 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 an 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 checkAndStorePasswordlessMessages( OnboardingState state, ) async { @@ -554,17 +367,29 @@ class PasswordlessRecoveryService { 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, + final alreadyReceivedIds = Int64List.fromList( + state.receivedShares.map((share) => share.messageId).toList(), ); - if (response == null || response.messages.isEmpty) { + late final server.Response_PasswordlessNotificationMessages response; + try { + final responseBytes = await RustApi.checkForPasswordlessNotification( + notificationId: state.notificationId!, + downloadAuthToken: state.downloadAuthToken!, + alreadyReceivedMessageIds: alreadyReceivedIds, + ); + response = server.Response_PasswordlessNotificationMessages.fromBuffer( + responseBytes, + ); + } catch (error) { + Log.error( + 'Failed to load passwordless recovery messages', + error: error, + ); + return false; + } + + if (response.messages.isEmpty) { return false; } diff --git a/lib/src/services/profile.service.dart b/lib/src/services/profile.service.dart deleted file mode 100644 index 8c37b7c5..00000000 --- a/lib/src/services/profile.service.dart +++ /dev/null @@ -1 +0,0 @@ -enum SetupProfile { standard, customized } diff --git a/lib/src/services/signal/identity.signal.dart b/lib/src/services/signal/identity.signal.dart index 6e018a63..23122682 100644 --- a/lib/src/services/signal/identity.signal.dart +++ b/lib/src/services/signal/identity.signal.dart @@ -30,10 +30,12 @@ class SignalIdentityService { await UserService.update((user) { user.signalLastSignedPreKeyUpdated = now; }); - final res = await apiService.updateSignedPreKey( - signedPreKey.id, - signedPreKey.getKeyPair().publicKey.serialize(), - signedPreKey.signature, + final res = await rustApiResult( + RustApi.updateSignedPreKey( + id: signedPreKey.id, + key: signedPreKey.getKeyPair().publicKey.serialize(), + signature: signedPreKey.signature, + ), ); if (res.isError) { Log.error('could not update the signed pre key: ${res.error}'); @@ -52,14 +54,16 @@ class SignalIdentityService { )) { final bundle = await RustSignal.generateBundle(); - final pqcRes = await apiService.uploadPqcPreKeys( - bundle.signedPreKeyId, - bundle.signedPreKeyPublic, - bundle.signedPreKeySignature, - bundle.kyberPreKeyId, - bundle.kyberPreKeyPublic, - bundle.kyberPreKeySignature, - [], + final pqcRes = await rustApiResult( + RustApi.uploadPqcPreKeys( + eccSignedPrekeyId: bundle.signedPreKeyId, + eccSignedPrekey: bundle.signedPreKeyPublic, + eccSignedPrekeySignature: bundle.signedPreKeySignature, + kyberSignedPrekeyId: bundle.kyberPreKeyId, + kyberSignedPrekey: bundle.kyberPreKeyPublic, + kyberSignedPrekeySignature: bundle.kyberPreKeySignature, + prekeys: const [], + ), ); if (pqcRes.isError) { diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index 6a715e0b..d6ce3d4c 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -206,7 +206,10 @@ Future getPublicKeyFromContact(int contactId) async { } Future handleSessionResync(int fromUserId) async { - final userData = await apiService.getUserById(fromUserId); + final userData = await rustApiProtobuf( + RustApi.getUserById(userId: fromUserId), + decodeUserData, + ); if (userData != null) { Log.info('Got new session data from the server to re-sync the session'); return processSignalUserData(userData); diff --git a/lib/src/services/user.service.dart b/lib/src/services/user.service.dart index 6d3c8018..f4b6cd10 100644 --- a/lib/src/services/user.service.dart +++ b/lib/src/services/user.service.dart @@ -1,16 +1,14 @@ import 'dart:async'; -import 'dart:convert'; import 'package:mutex/mutex.dart'; -import 'package:twonly/core/bridge/wrapper/key_manager.dart'; +import 'package:twonly/core/bridge/user_config.dart'; +import 'package:twonly/core/user_config.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/secure_storage.dart'; class UserService { - late UserData currentUser; + late UserConfig currentUser; bool isUserCreated = false; static final Mutex _updateProtection = Mutex(); @@ -18,43 +16,26 @@ class UserService { Stream get onUserUpdated => _userDataUpdateController.stream; Future tryInit() async { - final user = await getUser(); - if (user == null) return false; - userService.currentUser = user; - userService.isUserCreated = true; - return true; + final config = await UserConfigApi.load(); + if (config == null) return false; + _applyRustUserConfig(config, notify: false); + return isUserCreated; } - static Future getUser() async { + static Future getUser() async { try { - // 1. Try to load from KeyValueStore (user.json) - final userDataMap = await KeyValueStore.get('user'); - if (userDataMap != null) { - final userData = UserData.fromJson(userDataMap); - await RustKeyManager.setUserId(userId: userData.userId); - try { - // Ensure that the old userData is removed as it breaks the backup mechanism. - // This code can be removed when all users have updated to the latest version... - await SecureStorage.instance.delete(key: 'userData'); - } catch (e) { - Log.error('Could not delete user data from SecureStorage: $e'); - } - return userData; - } + final config = await UserConfigApi.load(); + if (config != null) return config; - // 2. If not found, try to load from SecureStorage (Migration path) + // One-time migration from the pre-user.json secure-storage format. final userDataJson = await SecureStorage.instance.read( key: 'userData', ); if (userDataJson != null) { - final userData = UserData.fromJson( - jsonDecode(userDataJson) as Map, - ); - - // 3. Run migration - await _migrateFromSecureStorage(userData); - return userData; + final migrated = await UserConfigApi.importJson(json: userDataJson); + await _removeLegacySecureStorageUser(); + return migrated; } return null; @@ -64,15 +45,7 @@ class UserService { } } - static Future _migrateFromSecureStorage(UserData userData) async { - await KeyValueStore.put('user', userData.toJson()); - - try { - await RustKeyManager.setUserId(userId: userData.userId); - } catch (e) { - Log.error('Could not set userId in RustKeyManager during migration: $e'); - } - + static Future _removeLegacySecureStorageUser() async { try { await SecureStorage.instance.delete(key: 'userData'); } catch (e) { @@ -83,35 +56,51 @@ class UserService { } static Future update( - void Function(UserData userData) updateUser, + void Function(UserConfig userData) updateUser, ) async { await _updateProtection.protect(() async { try { - final user = await getUser(); - if (user == null) return; + final config = await UserConfigApi.load(); + + if (config == null) { + throw Exception('User Config is missing'); + } + + final user = UserConfigApi.clone(config: config); if (user.defaultShowTime == 999999) { // This was the old version for infinity -> change it to null user.defaultShowTime = null; } updateUser(user); - await KeyValueStore.put('user', user.toJson()); - userService.currentUser = user; + + if (config == user) { + return; + } + + final normalized = await UserConfigApi.update( + base: config, + config: user, + ); + userService._applyRustUserConfig(normalized); } catch (e) { Log.error('Could not update the user: $e'); } }); - - userService.triggerUserUpdate(); } - static Future save(UserData user) async { - await KeyValueStore.put('user', user.toJson()); - try { - await RustKeyManager.setUserId(userId: user.userId); - } catch (e) { - Log.error('Could not set userId in RustKeyManager during save: $e'); - } - await userService.tryInit(); + static Future save(UserConfig user) async { + final normalized = await UserConfigApi.save(config: user); + userService._applyRustUserConfig(normalized); + } + + static Future handleRustUserConfigChanged(UserConfig config) async { + userService._applyRustUserConfig(config); + } + + void _applyRustUserConfig(UserConfig config, {bool notify = true}) { + currentUser = config; + isUserCreated = true; + if (notify) triggerUserUpdate(); } void triggerUserUpdate() { diff --git a/lib/src/services/user_discovery.service.dart b/lib/src/services/user_discovery.service.dart index 21c99c14..1c3803b1 100644 --- a/lib/src/services/user_discovery.service.dart +++ b/lib/src/services/user_discovery.service.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; @@ -9,7 +8,6 @@ import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart'; -import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -19,8 +17,9 @@ class UserDiscoveryService { .getNewAnnouncementsWithoutData(); for (final announcedUser in announcedUsers) { - final userdata = await apiService.getUserById( - announcedUser.announcedUserId, + final userdata = await rustApiProtobuf( + RustApi.getUserById(userId: announcedUser.announcedUserId), + decodeUserData, ); if (userdata == null) continue; if (!userdata.publicIdentityKey.equals( @@ -49,21 +48,6 @@ class UserDiscoveryService { } } - static bool isContactAllowed(Contact? c) { - if (c == null) return false; - final u = userService.currentUser; - // Only accepted users are allowed. - if (!c.accepted || c.blocked) return false; - if (c.mediaSendCounter < u.requiredSendImages) return false; - if (c.userDiscoveryExcluded) return false; - if (u.userDiscoveryRequiresManualApproval && - (c.userDiscoveryManualApproved == null || - !c.userDiscoveryManualApproved!)) { - return false; - } - return true; - } - static bool shouldRequestManualApproval(Contact c) { final u = userService.currentUser; if (!c.accepted || c.blocked) return false; @@ -79,20 +63,6 @@ class UserDiscoveryService { required int threshold, required bool sharePromotion, }) async { - Log.info('UserDiscoveryService: initializeOrUpdate started'); - final userId = userService.currentUser.userId; - final publicKey = await getUserPublicKey(); - Log.info('UserDiscoveryService: initializing Rust bridge'); - await FlutterUserDiscovery.initializeOrUpdate( - callbackId: isolateCallbackId, - threshold: threshold, - userId: userId, - publicKey: publicKey, - sharePromotion: sharePromotion, - ).timeout(const Duration(seconds: 8)); - Log.info( - 'UserDiscoveryService: Rust bridge initialized, updating UserService', - ); await UserService.update( (u) => u ..isUserDiscoveryEnabled = true @@ -120,14 +90,6 @@ class UserDiscoveryService { return UserDiscoveryVersion.fromBuffer(version); } - static Future getContactVersionTyped( - int contactId, - ) async { - final contact = await twonlyDB.contactsDao.getContactById(contactId); - if (contact == null || contact.userDiscoveryVersion == null) return null; - return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!); - } - static UserDiscoveryVersion? getContactVersionTypedFromContact( Contact contact, ) { @@ -135,89 +97,14 @@ class UserDiscoveryService { return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!); } - static Future shouldRequestNewMessages( - int fromUserId, - List receivedVersion, - ) async { - try { - return await FlutterUserDiscovery.shouldRequestNewMessages( - callbackId: isolateCallbackId, - contactId: fromUserId, - version: receivedVersion, - ).timeout(const Duration(seconds: 5)); - } catch (e) { - Log.error(e); - return null; - } - } - - static Future?> getNewMessages( - int fromUserId, - List receivedVersion, - ) async { - try { - return await FlutterUserDiscovery.getNewMessages( - callbackId: isolateCallbackId, - contactId: fromUserId, - receivedVersion: receivedVersion, - ).timeout(const Duration(seconds: 5)); - } catch (e) { - Log.error(e); - return null; - } - } - - static Future handleNewMessages( - int fromUserId, - List messages, - ) async { - try { - final verifications = await twonlyDB.keyVerificationDao - .getContactVerification(fromUserId); - - return await FlutterUserDiscovery.handleNewMessages( - callbackId: isolateCallbackId, - contactId: fromUserId, - messages: messages, - publicKeyVerifiedTimestamp: - verifications.lastOrNull?.createdAt.millisecondsSinceEpoch, - ).timeout(const Duration(seconds: 5)); - } catch (e) { - Log.error(e); - } - } - - static Future _removeDeletedContacts() async { - final subquery = twonlyDB.selectOnly(twonlyDB.contacts) - ..addColumns([twonlyDB.contacts.userId]) - ..where(twonlyDB.contacts.accountDeleted.equals(true)); - - await (twonlyDB.update( - twonlyDB.userDiscoveryOwnPromotions, - )..where((t) => t.contactId.isInQuery(subquery))).write( - UserDiscoveryOwnPromotionsCompanion(promotion: Value(Uint8List(0))), - ); - } - static Future changeExclusionForContact( int contactId, bool exclude, ) async { - // Remove old versions from the user... - await (twonlyDB.update( - twonlyDB.userDiscoveryOwnPromotions, - )..where((t) => t.contactId.equals(contactId))).write( - UserDiscoveryOwnPromotionsCompanion(promotion: Value(Uint8List(0))), - ); - - await twonlyDB.contactsDao.updateContact( - contactId, - ContactsCompanion( - userDiscoveryExcluded: Value(exclude), - userDiscoveryVersion: const Value( - null, // If the user is included again, this will trigger a new request of his original announcement - ), - ), + await FlutterUserDiscovery.changeExclusionForContact( + callbackId: isolateCallbackId, + contactId: contactId, + exclude: exclude, ); } @@ -226,35 +113,4 @@ class UserDiscoveryService { u.isUserDiscoveryEnabled = false; }); } - - static Future verifyInitializationOnStartup() async { - await _removeDeletedContacts(); - final configExists = File( - '${AppEnvironment.supportDir}/user_discovery_config.json', - ).existsSync(); - final hasShares = await (twonlyDB.select( - twonlyDB.userDiscoveryShares, - )..limit(1)).get().then((list) => list.isNotEmpty); - - if (userService.currentUser.isUserDiscoveryEnabled && - (userService.currentUser.userDiscoveryInitializationError || - !configExists || - !hasShares)) { - unawaited(() async { - try { - Log.info( - 'Retrying UserDiscovery initialization on startup (configExists: $configExists, hasShares: $hasShares)', - ); - await initializeOrUpdate( - threshold: userService.currentUser.userDiscoveryThreshold, - sharePromotion: userService.currentUser.userDiscoverySharePromotion, - ); - } catch (e) { - Log.error( - 'Failed to retry UserDiscovery initialization on startup: $e', - ); - } - }()); - } - } } diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart index 5c91a206..7d5a3258 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_ask_a_friend.entry.dart @@ -88,7 +88,7 @@ class _ChatAskAFriendEntryState extends State { _username = contact.displayName ?? contact.username; } else { // Fetch from API - final userdata = await apiService.getUserById(userId); + final userdata = await rustApiProtobuf(RustApi.getUserById(userId: userId), decodeUserData); if (userdata != null) { _username = utf8.decode(userdata.username); } @@ -122,7 +122,7 @@ class _ChatAskAFriendEntryState extends State { }); try { final userId = _data!.askAboutUserId.toInt(); - final userdata = await apiService.getUserById(userId); + final userdata = await rustApiProtobuf(RustApi.getUserById(userId: userId), decodeUserData); if (userdata != null) { await twonlyDB.contactsDao.insertOnConflictUpdate( ContactsCompanion( diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart index 04c1c2cc..0f2643e6 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_contacts.entry.dart @@ -117,8 +117,9 @@ class _ContactRowState extends State<_ContactRow> { }); try { - final userdata = await apiService.getUserById( - widget.contact.userId.toInt(), + final userdata = await rustApiProtobuf( + RustApi.getUserById(userId: widget.contact.userId.toInt()), + decodeUserData, ); if (userdata == null) return; diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index e52022e9..adea9d7b 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -369,7 +369,7 @@ class _MediaViewerViewState extends State { 'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}', ); unawaited( - apiService.downloadDone(currentMediaLocal.mediaFile.downloadToken!), + RustApi.downloadDone(token: currentMediaLocal.mediaFile.downloadToken!), ); if (currentMediaLocal.mediaFile.type == MediaType.video) { diff --git a/lib/src/visual/views/contact/add_new_contact.view.dart b/lib/src/visual/views/contact/add_new_contact.view.dart index dfd3acf4..fe6e33f1 100644 --- a/lib/src/visual/views/contact/add_new_contact.view.dart +++ b/lib/src/visual/views/contact/add_new_contact.view.dart @@ -117,7 +117,10 @@ class _SearchUsernameView extends State { _isLoading = true; }); - final userdata = await apiService.getUserData(username); + final userdata = await rustApiProtobuf( + RustApi.getUserData(username: username), + decodeUserData, + ); if (!mounted) return; setState(() { diff --git a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart index e85ae286..ff3f944b 100644 --- a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart +++ b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart @@ -47,7 +47,7 @@ class FriendSuggestionsComp extends StatelessWidget { ) async { Log.info('Requesting user via friend suggestions'); - final userdata = await apiService.getUserById(user.announcedUserId); + final userdata = await rustApiProtobuf(RustApi.getUserById(userId: user.announcedUserId), decodeUserData); if (userdata == null) { if (context.mounted) { diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index fe28593f..c4ec2d84 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -123,7 +123,7 @@ class _ContactViewState extends State { Future handleReportUser(Contact contact) async { final reason = await showReportDialog(context, contact); if (reason == null) return; - final res = await apiService.reportUser(contact.userId, reason); + final res = await rustApiResult(RustApi.reportUser(userId: contact.userId, reason: reason)); if (!mounted) return; if (res.isSuccess) { showSnackbar( @@ -281,7 +281,7 @@ class _ContactViewState extends State { icon: FontAwesomeIcons.arrowsRotate, text: 'Update Connection to V2 (PQXDH)', onTap: () async { - final userData = await apiService.getUserById(contact.userId); + final userData = await rustApiProtobuf(RustApi.getUserById(userId: contact.userId), decodeUserData); if (userData != null) { await processSignalUserData(userData); final updatedContact = await twonlyDB.contactsDao diff --git a/lib/src/visual/views/memories/memories.view.dart b/lib/src/visual/views/memories/memories.view.dart index 4c16b273..dc8757c2 100644 --- a/lib/src/visual/views/memories/memories.view.dart +++ b/lib/src/visual/views/memories/memories.view.dart @@ -46,7 +46,7 @@ class MemoriesViewState extends State } Future _checkUsage() async { - final usage = await apiService.getMemoriesUsage(); + final usage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage); if (usage != null && usage.maxBytes > 0 && usage.currentBytes >= usage.maxBytes) { @@ -320,7 +320,7 @@ class MemoriesViewState extends State if (item != null) { if (isCompletely) { item.mediaService.fullMediaRemoval(); - await apiService.deleteMemory(mediaId); + await RustApi.deleteMemory(mediaId: mediaId); await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId); } else { if (item.mediaService.storedPath.existsSync()) { diff --git a/lib/src/visual/views/memories/synchronized_viewer.view.dart b/lib/src/visual/views/memories/synchronized_viewer.view.dart index ed4c77f9..69555d39 100644 --- a/lib/src/visual/views/memories/synchronized_viewer.view.dart +++ b/lib/src/visual/views/memories/synchronized_viewer.view.dart @@ -177,7 +177,7 @@ class _SynchronizedImageViewerScreenState if (deleteCompletely) { item.mediaService.fullMediaRemoval(); - await apiService.deleteMemory(mediaId); + await RustApi.deleteMemory(mediaId: mediaId); await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId); widget.galleryItems.removeAt(_currentIndex); diff --git a/lib/src/visual/views/onboarding/recover_passwordless.view.dart b/lib/src/visual/views/onboarding/recover_passwordless.view.dart index bf5d3170..94c88213 100644 --- a/lib/src/visual/views/onboarding/recover_passwordless.view.dart +++ b/lib/src/visual/views/onboarding/recover_passwordless.view.dart @@ -15,9 +15,6 @@ import 'package:twonly/core/bridge/wrapper.dart' show RustUtils; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/keyvalue.keys.dart'; import 'package:twonly/src/model/json/onboarding_state.model.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/client/generated/passwordless_recovery.pb.dart'; import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/passwordless_recovery.service.dart'; @@ -262,10 +259,13 @@ class _RecoverPasswordlessState extends State { ); // Fetch serverKey - final res = await apiService.getServerKeyForPasswordlessRecovery( - userId: userId, - pinUnlockToken: reconstructed.pinUnlockToken, - pinProtectionKey: await pinKey.extractBytes(), + final res = await rustApiResult( + RustApi.getServerKeyForPasswordlessRecovery( + userId: userId, + serverKeyProtection: const [], + pinUnlockToken: Uint8List.fromList(reconstructed.pinUnlockToken), + pinProtectionKey: Uint8List.fromList(await pinKey.extractBytes()), + ), ); if (res.isError) { @@ -281,8 +281,7 @@ class _RecoverPasswordlessState extends State { return; } - final ok = res.value as server.Response_Ok; - serverKey = Uint8List.fromList(ok.passwordlessRecoveryServerKey); + serverKey = Uint8List.fromList(res.value!); } else if (reconstructed.hasEmailHint()) { final state = _onboardingState; if (state == null) return; @@ -302,10 +301,12 @@ class _RecoverPasswordlessState extends State { } // Fetch serverKey (sends recovery email) - final res = await apiService.getServerKeyForPasswordlessRecovery( - userId: userId, - email: email, - serverKeyProtection: reconstructed.serverKeyProtection, + final res = await rustApiResult( + RustApi.getServerKeyForPasswordlessRecovery( + userId: userId, + email: email, + serverKeyProtection: reconstructed.serverKeyProtection, + ), ); if (res.isError) { @@ -444,11 +445,13 @@ class _RecoverPasswordlessState extends State { Future _registerPasswordlessNotification(OnboardingState state) async { final fcmToken = await FirebaseMessaging.instance.getToken(); if (!mounted) return; - final res = await apiService.registerPasswordlessNotification( - notificationId: state.notificationId!, - downloadAuthToken: state.downloadAuthToken!, - langCode: Localizations.localeOf(context).languageCode, - googleFcm: fcmToken, + final res = await rustApiResult( + RustApi.registerPasswordlessNotification( + notificationId: state.notificationId!, + downloadAuthToken: state.downloadAuthToken!, + langCode: Localizations.localeOf(context).languageCode, + googleFcm: fcmToken, + ), ); if (res.isSuccess) { state.serverRegistered = true; diff --git a/lib/src/visual/views/onboarding/register.view.dart b/lib/src/visual/views/onboarding/register.view.dart index b024fe0e..ae71e3c1 100644 --- a/lib/src/visual/views/onboarding/register.view.dart +++ b/lib/src/visual/views/onboarding/register.view.dart @@ -1,15 +1,16 @@ // ignore_for_file: avoid_dynamic_calls import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; +import 'package:twonly/core/bridge/user_config.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/user.service.dart'; @@ -77,7 +78,12 @@ class _RegisterViewState extends State { if (proofOfWork != null) { proof = await proofOfWork!; } else { - final (pow, registrationDisabled) = await apiService.getProofOfWork(); + final proofResult = await rustApiResult(RustApi.getProofOfWork()); + final pow = proofResult.value == null + ? null + : decodeProofOfWork(proofResult.value!); + final registrationDisabled = + proofResult.error == ErrorCode.RegistrationDisabled; if (pow == null) { setState(() { _registrationDisabled = registrationDisabled; @@ -97,10 +103,17 @@ class _RegisterViewState extends State { var userId = 0; - final res = await apiService.register(username, null, proof); + final res = await rustApiResult( + RustApi.register( + username: username, + proofOfWork: proof, + langCode: ui.PlatformDispatcher.instance.locale.languageCode, + isIos: Platform.isIOS, + ), + ); if (res.isSuccess) { Log.info('Got user_id ${res.value} from server'); - userId = res.value.userid.toInt() as int; + userId = res.value!; } else { proofOfWork = null; if (res.error == ErrorCode.RegistrationDisabled) { @@ -120,7 +133,7 @@ class _RegisterViewState extends State { setState(() { _usernameErrorText = errorCodeToText( context, - res.error as ErrorCode, + res.error!, ); _isTryingToRegister = false; }); @@ -141,7 +154,7 @@ class _RegisterViewState extends State { await showAlertDialog( context, 'Oh no!', - errorCodeToText(context, res.error as ErrorCode), + errorCodeToText(context, res.error!), ); } return; @@ -151,11 +164,10 @@ class _RegisterViewState extends State { _isTryingToRegister = false; }); - final userData = UserData( + final userData = await UserConfigApi.create( userId: userId, username: username, displayName: username, - subscriptionPlan: 'Free', currentSetupPage: SetupPages.profile.name, appVersion: AppState.latestAppVersionId, ); diff --git a/lib/src/visual/views/onboarding/setup.view.dart b/lib/src/visual/views/onboarding/setup.view.dart index 285ab90d..53efd4b1 100644 --- a/lib/src/visual/views/onboarding/setup.view.dart +++ b/lib/src/visual/views/onboarding/setup.view.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:twonly/core/user_config.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/services/profile.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; diff --git a/lib/src/visual/views/onboarding/setup/components/profile_card.comp.dart b/lib/src/visual/views/onboarding/setup/components/profile_card.comp.dart index 04f5f8c8..05ce44f4 100644 --- a/lib/src/visual/views/onboarding/setup/components/profile_card.comp.dart +++ b/lib/src/visual/views/onboarding/setup/components/profile_card.comp.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:twonly/src/services/profile.service.dart'; +import 'package:twonly/core/user_config.dart'; import 'package:twonly/src/utils/misc.dart'; class SafetyProfileCard extends StatelessWidget { diff --git a/lib/src/visual/views/onboarding/setup/profile_selection.setup.dart b/lib/src/visual/views/onboarding/setup/profile_selection.setup.dart index f6bba8be..e65a515c 100644 --- a/lib/src/visual/views/onboarding/setup/profile_selection.setup.dart +++ b/lib/src/visual/views/onboarding/setup/profile_selection.setup.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:twonly/core/user_config.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/services/profile.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; diff --git a/lib/src/visual/views/settings/account.view.dart b/lib/src/visual/views/settings/account.view.dart index 6dd33624..24e301ff 100644 --- a/lib/src/visual/views/settings/account.view.dart +++ b/lib/src/visual/views/settings/account.view.dart @@ -56,7 +56,7 @@ class AccountView extends StatelessWidget { context.lang.settingsAccountDeleteModalBody, ); if (ok) { - final res = await apiService.deleteAccount(); + final res = await rustApiResult(RustApi.deleteAccount()); if (res.isError) { if (!context.mounted) return; showSnackbar( diff --git a/lib/src/visual/views/settings/backup/backup_settings.view.dart b/lib/src/visual/views/settings/backup/backup_settings.view.dart index 3c2dcff4..40ce8d62 100644 --- a/lib/src/visual/views/settings/backup/backup_settings.view.dart +++ b/lib/src/visual/views/settings/backup/backup_settings.view.dart @@ -54,7 +54,7 @@ class _BackupViewState extends State { Future _loadBackupStatus() async { setState(() => _isLoading = true); final status = await BackupService.getData(); - final memoriesUsage = await apiService.getMemoriesUsage(); + final memoriesUsage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage); if (!mounted) return; setState(() { _backupStatus = status; diff --git a/lib/src/visual/views/settings/backup/backup_utils.dart b/lib/src/visual/views/settings/backup/backup_utils.dart index dd401cc8..cb1c314c 100644 --- a/lib/src/visual/views/settings/backup/backup_utils.dart +++ b/lib/src/visual/views/settings/backup/backup_utils.dart @@ -82,7 +82,7 @@ Future promptAndDisableMemoriesBackup(BuildContext context) async { if (confirmed == true) { try { - await apiService.disableMemoriesBackup(); + await RustApi.disableMemoriesBackup(); final allMedias = await (twonlyDB.select( twonlyDB.mediaFiles, )..where((t) => t.stored.equals(true))).get(); diff --git a/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart b/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart index 98f4d4c4..3efae528 100644 --- a/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart +++ b/lib/src/visual/views/settings/backup/memories_backup_detail.view.dart @@ -29,7 +29,7 @@ class _MemoriesBackupDetailViewState extends State { } Future _loadStats() async { - final memoriesUsage = await apiService.getMemoriesUsage(); + final memoriesUsage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage); if (mounted) { setState(() { _memoriesUsage = memoriesUsage; diff --git a/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart b/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart index 62b3ebf2..80be0b3d 100644 --- a/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart +++ b/lib/src/visual/views/settings/data_and_storage/storage_contents.view.dart @@ -335,7 +335,7 @@ class _StorageContentsViewState extends State { try { if (deleteCompletely) { await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); - unawaited(apiService.deleteMemory(file.mediaId)); + unawaited(RustApi.deleteMemory(mediaId: file.mediaId)); MediaFileService(file).fullMediaRemoval(); } else { MediaFileService(file).storedPath.deleteSync(); @@ -384,7 +384,7 @@ class _StorageContentsViewState extends State { for (final file in selectedFiles) { if (deleteCompletely) { await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); - unawaited(apiService.deleteMemory(file.mediaId)); + unawaited(RustApi.deleteMemory(mediaId: file.mediaId)); MediaFileService(file).fullMediaRemoval(); } else { MediaFileService(file).storedPath.deleteSync(); diff --git a/lib/src/visual/views/settings/developer/retransmission_data.view.dart b/lib/src/visual/views/settings/developer/retransmission_data.view.dart index a7cc1386..93ad2f8a 100644 --- a/lib/src/visual/views/settings/developer/retransmission_data.view.dart +++ b/lib/src/visual/views/settings/developer/retransmission_data.view.dart @@ -7,10 +7,7 @@ import 'package:hashlib/random.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' - as pb; import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -186,24 +183,7 @@ class _RetransmissionDataViewState extends State { Text( 'MessageId: ${retrans.receipt.messageId}', ), - if (retrans.receipt.messageId != null) - FutureBuilder( - future: getPushNotificationFromEncryptedContent( - retrans.receipt.contactId, - retrans.receipt.messageId, - pb.EncryptedContent.fromBuffer( - pb.Message.fromBuffer( - retrans.receipt.message, - ).encryptedContent, - ), - ), - builder: (d, a) { - if (!a.hasData) return Container(); - return Text( - 'PushKind: ${a.data?.kind}', - ); - }, - ), + Text( 'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}', ), diff --git a/lib/src/visual/views/settings/notification.view.dart b/lib/src/visual/views/settings/notification.view.dart index cb81a962..0738969b 100644 --- a/lib/src/visual/views/settings/notification.view.dart +++ b/lib/src/visual/views/settings/notification.view.dart @@ -1,14 +1,9 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hashlib/random.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; @@ -47,8 +42,6 @@ class _NotificationViewState extends State { await FcmNotificationService.initFCMAfterAuthenticated(force: true); - await setupNotificationWithUsers(force: true); - if (!mounted) return; if (userService.currentUser.fcmToken == null) { @@ -66,18 +59,6 @@ class _NotificationViewState extends State { ); if (run) { - final pushData = await encryptPushNotification( - userService.currentUser.userId, - PushNotification( - messageId: uuid.v4(), - kind: PushKind.TEST_NOTIFICATION, - ), - ); - await apiService.sendTextMessage( - userService.currentUser.userId, - Uint8List(0), - pushData, - ); _troubleshootingDidRun = true; } } diff --git a/lib/src/visual/views/settings/profile/profile.view.dart b/lib/src/visual/views/settings/profile/profile.view.dart index 34f80400..30f4383f 100644 --- a/lib/src/visual/views/settings/profile/profile.view.dart +++ b/lib/src/visual/views/settings/profile/profile.view.dart @@ -6,7 +6,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -62,7 +61,9 @@ class _ProfileViewState extends State { filteredUsername = filteredUsername.substring(0, 12); } - final result = await apiService.changeUsername(filteredUsername); + final result = await rustApiResult( + RustApi.changeUsername(username: filteredUsername), + ); if (result.isError) { if (!mounted) return; diff --git a/lib/src/visual/views/settings/subscription/additional_users.view.dart b/lib/src/visual/views/settings/subscription/additional_users.view.dart index 8ec1d889..26f4821a 100644 --- a/lib/src/visual/views/settings/subscription/additional_users.view.dart +++ b/lib/src/visual/views/settings/subscription/additional_users.view.dart @@ -1,12 +1,10 @@ import 'dart:async'; -import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/services/subscription.service.dart'; @@ -50,7 +48,10 @@ class _AdditionalUsersViewState extends State { Future initAsync({required bool force}) async { if (force) { - ballance = await apiService.loadPlanBalance(); + ballance = await rustApiProtobuf( + RustApi.loadPlanBalance(), + decodePlanBalance, + ); _unusedAdditionalAccounts = _planLimit - (ballance?.additionalAccounts.length ?? _planLimit); } @@ -72,7 +73,9 @@ class _AdditionalUsersViewState extends State { as List?; if (selectedUserIds == null) return; for (final selectedUserId in selectedUserIds) { - final res = await apiService.addAdditionalUser(Int64(selectedUserId)); + final res = await rustApiResult( + RustApi.addAdditionalUser(userId: selectedUserId), + ); if (res.isError && mounted) { final contact = await twonlyDB.contactsDao.getContactById( selectedUserId, @@ -220,8 +223,10 @@ class _AdditionalAccountState extends State { context.lang.additionalUserRemoveDesc, ); if (remove) { - final res = await apiService.removeAdditionalUser( - widget.account.userId, + final res = await rustApiResult( + RustApi.removeAdditionalUser( + userId: widget.account.userId.toInt(), + ), ); if (!context.mounted) return; if (res.isSuccess) { @@ -231,7 +236,7 @@ class _AdditionalAccountState extends State { context, errorCodeToText( context, - res.error as ErrorCode, + res.error!, ), ); } diff --git a/lib/src/visual/views/settings/subscription/subscription.view.dart b/lib/src/visual/views/settings/subscription/subscription.view.dart index 1be08eaf..1e8e8a42 100644 --- a/lib/src/visual/views/settings/subscription/subscription.view.dart +++ b/lib/src/visual/views/settings/subscription/subscription.view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; @@ -35,7 +36,10 @@ class _SubscriptionViewState extends State { } Future initAsync() async { - ballance = await apiService.loadPlanBalance(); + ballance = await rustApiProtobuf( + RustApi.loadPlanBalance(), + decodePlanBalance, + ); if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) { final ownerId = ballance!.additionalAccountOwnerId.toInt(); final contact = await twonlyDB.contactsDao @@ -49,7 +53,7 @@ class _SubscriptionViewState extends State { } if (!mounted) return; setState(() {}); - await apiService.forceIpaCheck(); + await RustApi.forceIpaCheck(); } @override diff --git a/pubspec.lock b/pubspec.lock index 18a9dc74..11acd7c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -866,7 +866,7 @@ packages: source: hosted version: "4.0.0" freezed_annotation: - dependency: "direct main" + dependency: transitive description: name: freezed_annotation sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" diff --git a/pubspec.yaml b/pubspec.yaml index 8ff64fcd..b094f043 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -258,4 +258,3 @@ flutter: fonts: - asset: assets/fonts/NotoColorEmoji.ttf - diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 4238e15f..d685e310 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -97,6 +97,7 @@ dependencies = [ "anyhow", "atomic", "backtrace", + "chrono", ] [[package]] @@ -1093,6 +1094,7 @@ dependencies = [ "build-target", "bytemuck", "byteorder", + "chrono", "console_error_panic_hook", "dart-sys", "delegate-attr", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 69be76db..92b49797 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib", "staticlib", "rlib"] [dependencies] -flutter_rust_bridge = "=2.12.0" +flutter_rust_bridge = { version = "=2.12.0", features = ["chrono"] } thiserror = "2.0.18" sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [ "runtime-tokio", diff --git a/rust/build.rs b/rust/build.rs index 3bd84a09..d497f72e 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -25,7 +25,7 @@ fn main() -> Result<()> { .include_file("websocket_protocol.rs") .compile_protos(&websocket_protos, &[websocket_proto_root])?; - prost_build::compile_protos(&["src/user_discovery/types.proto"], &["src/"])?; + prost_build::compile_protos(&["models/user_discovery.proto"], &["src/"])?; prost_build::Config::new() .include_file("client_messages.rs") .compile_protos( diff --git a/rust/src/api/messages/incoming/client2client/additional_data.rs b/rust/src/api/messages/incoming/client2client/additional_data.rs index 34f589a1..171b0d51 100644 --- a/rust/src/api/messages/incoming/client2client/additional_data.rs +++ b/rust/src/api/messages/incoming/client2client/additional_data.rs @@ -91,11 +91,10 @@ async fn verify_shared_contacts( .await?; let verified_at = chrono::Utc::now().timestamp_millis(); - ctx.get_user_discovery() .get() .await - .update_verification_state_for_user(contact.user_id, Some(verified_at)) + .update_verification_state_for_user(contact.user_id, Some(verified_at), tr) .await?; tracing::info!("verified a contact from shared additional data"); diff --git a/rust/src/api/messages/incoming/client2client/contact.rs b/rust/src/api/messages/incoming/client2client/contact.rs index e9ef55fd..613b078d 100644 --- a/rust/src/api/messages/incoming/client2client/contact.rs +++ b/rust/src/api/messages/incoming/client2client/contact.rs @@ -17,8 +17,68 @@ use crate::error::{Result, TwonlyError}; use crate::user_config::UserConfig; use encrypted_content::contact_request::Type; use sqlx::{Sqlite, Transaction}; +use std::collections::HashMap; use std::io::Write as _; -use std::sync::Arc; +use std::sync::{Arc, Mutex, OnceLock}; + +static REQUESTED_PROFILES: OnceLock>> = OnceLock::new(); + +pub(crate) async fn check_for_profile_update( + t: &mut Transaction<'_, Sqlite>, + from_user_id: i64, + content: &EncryptedContent, +) -> Result<()> { + let Some(sender_profile_counter) = content.sender_profile_counter else { + return Ok(()); + }; + + let current_counter = sqlx::query_scalar!( + "SELECT sender_profile_counter FROM contacts WHERE user_id = ?", + from_user_id, + ) + .fetch_optional(&mut **t) + .await? + .unwrap_or(0); + + if content.contact_update.is_some() || sender_profile_counter <= current_counter { + return Ok(()); + } + + let should_request = { + let mut requested = REQUESTED_PROFILES + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .unwrap(); + let last_requested = requested.get(&from_user_id).copied().unwrap_or(0); + + if sender_profile_counter > last_requested { + requested.insert(from_user_id, sender_profile_counter); + true + } else { + false + } + }; + + if should_request { + queue_encrypted_content( + t, + from_user_id, + EncryptedContent { + contact_update: Some(encrypted_content::ContactUpdate { + r#type: encrypted_content::contact_update::Type::Request as i32, + username: None, + display_name: None, + avatar_svg_compressed: None, + }), + ..Default::default() + }, + true, + ) + .await?; + } + + Ok(()) +} pub(crate) async fn handle_contact_request( ctx: &Arc, @@ -73,6 +133,7 @@ pub(crate) async fn handle_contact_request( ))); } }; + let username = user .username .ok_or_else(|| TwonlyError::Generic("user response has no username".into()))?; @@ -149,8 +210,8 @@ pub(crate) async fn handle_contact_update( EncryptedContent { contact_update: Some(encrypted_content::ContactUpdate { r#type: encrypted_content::contact_update::Type::Update as i32, - username: user.username, - display_name: user.display_name, + username: Some(user.username), + display_name: Some(user.display_name), avatar_svg_compressed, }), sender_profile_counter: Some(user.avatar_counter), diff --git a/rust/src/api/messages/incoming/client2client/errors.rs b/rust/src/api/messages/incoming/client2client/errors.rs index 10099024..e0ce33fe 100644 --- a/rust/src/api/messages/incoming/client2client/errors.rs +++ b/rust/src/api/messages/incoming/client2client/errors.rs @@ -4,19 +4,25 @@ */ use crate::api::proto::client::encrypted_content; +use crate::context::Context; use crate::database::app::tables::UpdateContact; use crate::error::Result; +use crate::services::groups::GroupService; use encrypted_content::error_messages::Type; use sqlx::{Sqlite, Transaction}; +use std::sync::Arc; pub(crate) async fn handle_error_message( - transaction: &mut Transaction<'_, Sqlite>, + ctx: &Arc, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, - _receipt_id: &str, group_id: Option<&str>, error: encrypted_content::ErrorMessages, ) -> Result<()> { - match Type::try_from(error.r#type)? { + let error_type = Type::try_from(error.r#type)?; + tracing::warn!(?error_type, from_user_id, "received client error message"); + + match error_type { Type::ErrorProcessingMessageCreatedAccountRequestInstead => { sqlx::query!( r#" @@ -27,30 +33,27 @@ pub(crate) async fn handle_error_message( error.related_receipt_id, from_user_id, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; + UpdateContact::builder() .user_id(from_user_id) .accepted(false) .requested(true) .build() - .update(transaction) + .update(t) .await?; } Type::GroupNotFoundOrNotAMember => { if let Some(group_id) = group_id { - if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { - let group_id = group_id.to_owned(); - let related_receipt_id = error.related_receipt_id.clone(); - tokio::spawn(async move { - (callbacks.api.group_membership_error)( - from_user_id, - group_id, - related_receipt_id, - ) - .await; - }); - } + GroupService::new(ctx) + .handle_membership_error( + t, + from_user_id, + group_id.to_owned(), + error.related_receipt_id, + ) + .await?; } } Type::SessionOutOfSync | Type::UnknownMessageType => {} diff --git a/rust/src/api/messages/incoming/client2client/groups.rs b/rust/src/api/messages/incoming/client2client/groups.rs index 74d32bbf..0a1f4035 100644 --- a/rust/src/api/messages/incoming/client2client/groups.rs +++ b/rust/src/api/messages/incoming/client2client/groups.rs @@ -3,31 +3,25 @@ * */ +use std::sync::Arc; + +use crate::api::messages::incoming::client2client::messages::queue_encrypted_content; use crate::api::proto::client::encrypted_content; +use crate::context::Context; +use crate::database::app::tables::{Contact, Group}; use crate::error::{Result, TwonlyError}; -use crate::utils::{milliseconds_to_seconds, new_uuid_v4}; +use crate::services::groups::GroupService; +use crate::utils::{is_today, milliseconds_to_seconds, new_uuid_v4}; use rand::SeedableRng; use sqlx::{Sqlite, Transaction}; pub(crate) async fn ensure_group_member( - transaction: &mut Transaction<'_, Sqlite>, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, group_id: &str, ) -> Result<()> { - let allowed = sqlx::query_scalar!( - r#" - SELECT EXISTS( - SELECT 1 - FROM group_members - WHERE group_id = ? AND contact_id = ? - ) - "#, - group_id, - from_user_id, - ) - .fetch_one(&mut **transaction) - .await? - != 0; + let allowed = Group::is_member(t, group_id, from_user_id).await?; + if !allowed { return Err(TwonlyError::Generic(format!( "user {from_user_id} is not a member of group {group_id}" @@ -37,32 +31,19 @@ pub(crate) async fn ensure_group_member( } pub(crate) async fn handle_group_create( - transaction: &mut Transaction<'_, Sqlite>, + ctx: &std::sync::Arc, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, group_id: &str, create: encrypted_content::GroupCreate, ) -> Result<()> { - let contact_exists = sqlx::query_scalar!( - r#" - SELECT EXISTS( - SELECT 1 FROM contacts WHERE user_id = ? - ) - "#, - from_user_id, - ) - .fetch_one(&mut **transaction) - .await? - != 0; - if !contact_exists { - return Err(TwonlyError::Generic( - "only known contacts may create a group".into(), - )); - } + Contact::ensure_exists(t, from_user_id).await?; let mut rng = rand::rngs::StdRng::from_os_rng(); let identity = libsignal_protocol::IdentityKeyPair::generate(&mut rng); let private_key = identity.serialize().to_vec(); let group_name = create.group_name.unwrap_or_default(); + sqlx::query!( r#" INSERT INTO groups( @@ -83,8 +64,9 @@ pub(crate) async fn handle_group_create( private_key, group_name, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; + sqlx::query!( r#" INSERT INTO group_members(group_id, contact_id, group_public_key) @@ -96,8 +78,9 @@ pub(crate) async fn handle_group_create( from_user_id, create.group_public_key, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; + sqlx::query!( r#"INSERT INTO group_histories(group_history_id, group_id, contact_id, type) VALUES (?, ?, ?, 'addMember')"#, @@ -105,42 +88,24 @@ pub(crate) async fn handle_group_create( group_id, from_user_id, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; - if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { - let group_id = group_id.to_owned(); - tokio::spawn(async move { - (callbacks.api.group_state_refresh)(group_id, true).await; - }); - } + + GroupService::new(ctx) + .refresh_group_state(t, group_id.to_owned(), true) + .await; + Ok(()) } pub(crate) async fn handle_group_join( - transaction: &mut Transaction<'_, Sqlite>, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, group_id: &str, join: encrypted_content::GroupJoin, ) -> Result<()> { - let group_exists = sqlx::query_scalar!( - r#"SELECT EXISTS(SELECT 1 FROM groups WHERE group_id = ?)"#, - group_id, - ) - .fetch_one(&mut **transaction) - .await? - != 0; - let contact_exists = sqlx::query_scalar!( - r#"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ?)"#, - from_user_id, - ) - .fetch_one(&mut **transaction) - .await? - != 0; - if !group_exists || !contact_exists { - return Err(TwonlyError::Generic( - "group join arrived before group/contact state".into(), - )); - } + Contact::ensure_exists(t, from_user_id).await?; + Group::ensure_exists(t, group_id).await?; sqlx::query!( r#" @@ -153,13 +118,14 @@ pub(crate) async fn handle_group_join( from_user_id, join.group_public_key, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; + Ok(()) } pub(crate) async fn handle_resend_group_public_key( - transaction: &mut Transaction<'_, Sqlite>, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, group_id: &str, ) -> Result<()> { @@ -171,18 +137,19 @@ pub(crate) async fn handle_resend_group_public_key( "#, group_id, ) - .fetch_optional(&mut **transaction) + .fetch_optional(&mut **t) .await? .flatten(); + let Some(private_key) = private_key else { return Err(TwonlyError::Generic(format!( "cannot resend the group public key for {group_id} to {from_user_id}" ))); }; - let identity = libsignal_protocol::IdentityKeyPair::try_from(private_key.as_slice()) - .map_err(|error| TwonlyError::Signal(error.to_string()))?; - super::messages::queue_encrypted_content( - transaction, + + let identity = libsignal_protocol::IdentityKeyPair::try_from(private_key.as_slice())?; + queue_encrypted_content( + t, from_user_id, crate::api::proto::client::EncryptedContent { group_id: Some(group_id.to_owned()), @@ -194,31 +161,25 @@ pub(crate) async fn handle_resend_group_public_key( true, ) .await?; + Ok(()) } pub(crate) async fn handle_group_update( - transaction: &mut Transaction<'_, Sqlite>, + ctx: &Arc, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, group_id: &str, update: encrypted_content::GroupUpdate, ) -> Result<()> { - let is_direct = sqlx::query_scalar!( - "SELECT is_direct_chat FROM groups WHERE group_id = ?", - group_id - ) - .fetch_optional(&mut **transaction) - .await? - .unwrap_or(0) - != 0; + let is_direct = Group::is_direct_chat(t, group_id).await?; + if !is_direct { - if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { - let group_id = group_id.to_owned(); - tokio::spawn(async move { - (callbacks.api.group_state_refresh)(group_id, false).await; - }); - } + GroupService::new(ctx) + .refresh_group_state(t, group_id.to_owned(), false) + .await; } + if update.group_action_type == "updatedGroupName" { sqlx::query!( r#" @@ -229,7 +190,7 @@ pub(crate) async fn handle_group_update( update.new_group_name, group_id, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; } else if update.group_action_type == "changeDisplayMaxTime" && is_direct { sqlx::query!( @@ -243,7 +204,7 @@ pub(crate) async fn handle_group_update( update.new_delete_messages_after_milliseconds, group_id, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; } @@ -267,43 +228,66 @@ pub(crate) async fn handle_group_update( update.new_delete_messages_after_milliseconds, update.group_action_type, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; + Ok(()) } pub(crate) async fn handle_flame_sync( - transaction: &mut Transaction<'_, Sqlite>, + t: &mut Transaction<'_, Sqlite>, group_id: &str, flame: encrypted_content::FlameSync, ) -> Result<()> { let last_flame_counter_change = milliseconds_to_seconds(flame.last_flame_counter_change); + + let Some(group) = sqlx::query!( + r#" + SELECT last_flame_counter_change, flame_counter, max_flame_counter + FROM groups + WHERE group_id = ? + "#, + group_id, + ) + .fetch_optional(&mut **t) + .await? + else { + return Ok(()); + }; + + let Some(group_last_flame_counter_change) = group.last_flame_counter_change else { + return Ok(()); + }; + + let update_counters = flame.force_update + || (is_today(group_last_flame_counter_change) && is_today(last_flame_counter_change)); + + let flame_counter = if update_counters { + group.flame_counter.max(flame.flame_counter) + } else { + group.flame_counter + }; + let max_flame_counter = if update_counters { + group.max_flame_counter.max(flame.flame_counter) + } else { + group.max_flame_counter + }; + sqlx::query!( r#" UPDATE groups SET also_best_friend = ?, - flame_counter = CASE WHEN ( - (date(last_flame_counter_change, 'unixepoch', 'localtime') = date('now', 'localtime') - AND date(?, 'unixepoch', 'localtime') = date('now', 'localtime')) - OR ? - ) THEN MAX(flame_counter, ?) ELSE flame_counter END, - max_flame_counter = CASE WHEN ( - (date(last_flame_counter_change, 'unixepoch', 'localtime') = date('now', 'localtime') - AND date(?, 'unixepoch', 'localtime') = date('now', 'localtime')) - OR ? - ) THEN MAX(max_flame_counter, ?) ELSE max_flame_counter END - WHERE group_id = ? AND last_flame_counter_change IS NOT NULL + flame_counter = ?, + max_flame_counter = ? + WHERE group_id = ? "#, flame.best_friend, - last_flame_counter_change, - flame.force_update, - flame.flame_counter, - last_flame_counter_change, - flame.force_update, - flame.flame_counter, + flame_counter, + max_flame_counter, group_id, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; + Ok(()) } diff --git a/rust/src/api/messages/incoming/client2client/media.rs b/rust/src/api/messages/incoming/client2client/media.rs index c5ed5f05..33afcd6c 100644 --- a/rust/src/api/messages/incoming/client2client/media.rs +++ b/rust/src/api/messages/incoming/client2client/media.rs @@ -5,13 +5,31 @@ use crate::api::proto::client::encrypted_content; use crate::bridge::callbacks::get_callbacks; +use crate::context::Context; use crate::database::app::tables::Group; use crate::error::{Result, TwonlyError}; +use crate::services::mediafiles::MediaFileService; use crate::utils::{milliseconds_to_seconds, new_uuid_v4}; use encrypted_content::media::Type as MediaType; use encrypted_content::media_update::Type as MediaUpdateType; use sqlx::{Sqlite, Transaction}; +fn spawn_media_download(media_id: String) { + let Ok(ctx) = Context::get_static() else { + tracing::warn!(media_id, "could not start media download without context"); + return; + }; + let ctx = ctx.clone(); + tokio::spawn(async move { + if let Err(error) = MediaFileService::new(&ctx) + .download_when_available(&media_id) + .await + { + tracing::warn!(media_id, %error, "media download failed"); + } + }); +} + fn spawn_media_action(kind: &'static str, media_id: String, contact_id: i64, message_id: String) { if let Ok(callbacks) = get_callbacks() { tokio::spawn(async move { @@ -65,7 +83,7 @@ pub(crate) async fn handle_media( .execute(&mut **t) .await?; - spawn_media_action("download", media_id, from_user_id, media.sender_message_id); + spawn_media_download(media_id); return Ok(()); } @@ -122,7 +140,7 @@ pub(crate) async fn handle_media( .execute(&mut **t) .await?; - spawn_media_action("download", media_id, from_user_id, media.sender_message_id); + spawn_media_download(media_id); return Ok(()); } @@ -178,7 +196,9 @@ pub(crate) async fn handle_media( ) .execute(&mut **t) .await?; + Group::increase_last_message_exchange(t, group_id, timestamp).await?; + if let Ok(callbacks) = get_callbacks() { let group_id = group_id.to_owned(); let timestamp = media.timestamp; @@ -186,7 +206,9 @@ pub(crate) async fn handle_media( (callbacks.api.media_received)(group_id, timestamp).await; }); } - spawn_media_action("download", media_id, from_user_id, media.sender_message_id); + + spawn_media_download(media_id); + Ok(()) } diff --git a/rust/src/api/messages/incoming/client2client/messages.rs b/rust/src/api/messages/incoming/client2client/messages.rs index 3831dc33..c0222fef 100644 --- a/rust/src/api/messages/incoming/client2client/messages.rs +++ b/rust/src/api/messages/incoming/client2client/messages.rs @@ -4,13 +4,12 @@ */ use super::handle_encrypted; -use crate::api::proto::client::{self as proto, encrypted_content}; - +use crate::api::proto::client::{self as proto}; use crate::api::Server; use crate::bridge::api::ServerResult; use crate::bridge::callbacks::get_callbacks; use crate::context::Context; -use crate::database::app::tables::{Contact, NewReceipt, Receipt}; +use crate::database::app::tables::{Contact, MediaFile, NewReceipt, Receipt}; use crate::error::{twonly_error, Result, TwonlyError}; use crate::utils::new_uuid_v4; use prost::Message as ProstMessage; @@ -25,26 +24,6 @@ use std::{collections::HashMap, sync::LazyLock}; static ALREADY_QUEUED_RECEIPTS: LazyLock>> = LazyLock::new(|| std::sync::Mutex::new(HashMap::new())); -async fn native_push_data( - _ctx: &Context, - contact_id: i64, - message_id: Option, - plaintext: &[u8], - message_type: i32, -) -> Result>> { - let callbacks = match get_callbacks() { - Ok(callbacks) => callbacks, - Err(TwonlyError::MissingCallbackInitialization) => { - return Ok(None); - } - Err(error) => return Err(error), - }; - Ok( - (callbacks.api.create_push_data)(contact_id, message_id, plaintext.to_vec(), message_type) - .await, - ) -} - pub(crate) async fn queue_encrypted_content( transaction: &mut Transaction<'_, Sqlite>, target_user_id: i64, @@ -103,7 +82,7 @@ pub(crate) async fn process_encrypted_or_queue_error( let Some(error_type) = error_type else { return Err(error); }; - let outgoing_receipt_id = uuid::Uuid::new_v4().to_string(); + let outgoing_receipt_id = new_uuid_v4(); let response_content = proto::EncryptedContent { group_id, error_messages: Some(proto::encrypted_content::ErrorMessages { @@ -318,18 +297,7 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc, receipt_id: &str) -> let message_type = proto::message::Type::try_from(message.r#type)?; - let push_data = if row.retry_count == 0 { - native_push_data( - ctx, - row.contact_id, - row.message_id.clone(), - &message.encrypted_content.clone().unwrap_or_default(), - message.r#type, - ) - .await? - } else { - None - }; + let push_data: Option> = None; match message_type { proto::message::Type::Ciphertext | proto::message::Type::PrekeyBundle => { @@ -422,18 +390,7 @@ pub(crate) async fn prepare_queued_receipt( let mut message = proto::Message::decode(row.message.as_slice()) .map_err(|error| TwonlyError::Generic(format!("invalid queued message: {error}")))?; message.receipt_id = receipt_id.to_owned(); - let push_data = if row.retry_count == 0 { - native_push_data( - ctx, - row.contact_id, - row.message_id, - &message.encrypted_content.clone().unwrap_or_default(), - message.r#type, - ) - .await? - } else { - None - }; + let push_data: Option> = None; match proto::message::Type::try_from(message.r#type) .map_err(|_| TwonlyError::Generic("queued message has invalid type".into()))? @@ -593,6 +550,8 @@ pub(crate) async fn handle_sender_delivery_receipt( ) .execute(&mut **transaction) .await?; + + MediaFile::handle_response_from_receiver(transaction, &message_id).await?; } sqlx::query!( r#" @@ -635,144 +594,3 @@ pub async fn handle_plaintext_content( } Ok(None) } - -pub(crate) async fn handle_message_update( - transaction: &mut Transaction<'_, Sqlite>, - from_user_id: i64, - update: encrypted_content::MessageUpdate, -) -> Result<()> { - use encrypted_content::message_update::Type; - let timestamp = crate::utils::milliseconds_to_seconds(update.timestamp); - - match Type::try_from(update.r#type) - .map_err(|_| TwonlyError::Generic("invalid message update".into()))? - { - Type::Opened => { - for message_id in update.multiple_target_message_ids { - let action_at = sqlx::query_scalar::<_, i64>( - "SELECT MAX(created_at, ?) FROM messages WHERE message_id = ?", - ) - .bind(timestamp) - .bind(&message_id) - .fetch_optional(&mut **transaction) - .await?; - let Some(action_at) = action_at else { continue }; - sqlx::query!( - r#" - INSERT INTO message_actions(message_id, contact_id, type, action_at) - VALUES (?, ?, 'openedAt', ?) - ON CONFLICT(message_id, contact_id, type) - DO UPDATE SET action_at = excluded.action_at - "#, - message_id, - from_user_id, - action_at, - ) - .execute(&mut **transaction) - .await?; - sqlx::query!( - r#"UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS( - SELECT 1 FROM group_members gm - WHERE gm.group_id = messages.group_id AND NOT EXISTS( - SELECT 1 FROM message_actions ma - WHERE ma.message_id = messages.message_id - AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt' - ) - ) THEN ? ELSE NULL END - WHERE message_id = ?"#, - action_at, - action_at, - message_id, - ) - .execute(&mut **transaction) - .await?; - } - } - Type::Delete => { - let media_id = sqlx::query_scalar!( - "SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?", - update.sender_message_id, - from_user_id, - ) - .fetch_optional(&mut **transaction) - .await? - .flatten(); - sqlx::query!( - "DELETE FROM message_histories WHERE message_id = ?", - update.sender_message_id - ) - .execute(&mut **transaction) - .await?; - sqlx::query!( - "DELETE FROM receipts WHERE message_id = ?", - update.sender_message_id - ) - .execute(&mut **transaction) - .await?; - sqlx::query!( - r#" - UPDATE messages - SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ? - WHERE message_id = ? AND sender_id = ? - "#, - timestamp, - update.sender_message_id, - from_user_id, - ) - .execute(&mut **transaction) - .await?; - if let Some(media_id) = media_id { - let references = sqlx::query_scalar!( - "SELECT COUNT(*) FROM messages WHERE media_id = ?", - media_id, - ) - .fetch_one(&mut **transaction) - .await?; - if references == 0 { - sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id) - .execute(&mut **transaction) - .await?; - if let Ok(callbacks) = get_callbacks() { - if let Some(message_id) = update.sender_message_id.clone() { - tokio::spawn(async move { - (callbacks.api.media_action)( - "delete".into(), - media_id, - from_user_id, - message_id, - ) - .await; - }); - } - } - } - } - } - Type::EditText => { - sqlx::query!( - r#"INSERT INTO message_histories(message_id, content, created_at) - SELECT message_id, content, ? FROM messages - WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL"#, - timestamp, - update.sender_message_id, - from_user_id, - ) - .execute(&mut **transaction) - .await?; - sqlx::query!( - r#" - UPDATE messages - SET content = ?, modified_at = ? - WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL - "#, - update.text, - timestamp, - update.sender_message_id, - from_user_id, - ) - .execute(&mut **transaction) - .await?; - } - } - Ok(()) -} diff --git a/rust/src/api/messages/incoming/client2client/mod.rs b/rust/src/api/messages/incoming/client2client/mod.rs index fae930e3..1fd4d59b 100644 --- a/rust/src/api/messages/incoming/client2client/mod.rs +++ b/rust/src/api/messages/incoming/client2client/mod.rs @@ -9,21 +9,17 @@ use crate::context::Context; use crate::database::app::tables::{Contact, Group, Receipt}; use crate::error::{Result, TwonlyError}; use sqlx::{Sqlite, Transaction}; -use std::collections::HashMap; -use std::sync::{Arc, Mutex, OnceLock}; +use std::sync::Arc; use typing_indicator::handle_typing_indicator; -static REQUESTED_PROFILES: OnceLock>> = OnceLock::new(); - mod additional_data; pub(crate) mod contact; mod errors; mod groups; mod media; pub mod messages; -mod pushkeys; mod reaction; -mod recovery; +pub(crate) mod recovery; mod text_message; mod typing_indicator; mod user_discovery; @@ -34,64 +30,21 @@ mod verification; /// this dispatcher. pub(crate) async fn handle_encrypted( ctx: &Arc, - tr: &mut Transaction<'_, Sqlite>, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, receipt_id: &str, content: proto::EncryptedContent, ) -> Result<()> { - Receipt::mark_all_for_retry(tr, from_user_id).await?; + Receipt::mark_all_for_retry(t, from_user_id).await?; if let Some(version) = content.sender_user_discovery_version.clone() { - user_discovery::check_sender_version(ctx, tr, from_user_id, version).await?; + user_discovery::check_sender_version(ctx, t, from_user_id, version).await?; } - if let Some(sender_profile_counter) = content.sender_profile_counter { - let current_counter = sqlx::query_scalar!( - "SELECT sender_profile_counter FROM contacts WHERE user_id = ?", - from_user_id, - ) - .fetch_optional(&mut **tr) - .await? - .unwrap_or(0); - - if content.contact_update.is_none() && sender_profile_counter > current_counter { - let should_request = { - let mut requested = REQUESTED_PROFILES - .get_or_init(|| Mutex::new(HashMap::new())) - .lock() - .unwrap(); - let last_requested = requested.get(&from_user_id).copied().unwrap_or(0); - - if sender_profile_counter > last_requested { - requested.insert(from_user_id, sender_profile_counter); - true - } else { - false - } - }; - - if should_request { - messages::queue_encrypted_content( - tr, - from_user_id, - proto::EncryptedContent { - contact_update: Some(proto::encrypted_content::ContactUpdate { - r#type: proto::encrypted_content::contact_update::Type::Request as i32, - username: None, - display_name: None, - avatar_svg_compressed: None, - }), - ..Default::default() - }, - true, - ) - .await?; - } - } - } + contact::check_for_profile_update(t, from_user_id, &content).await?; if content.ask_for_friend_promotions == Some(true) { - Contact::update_ask_for_friend_promotions(tr, from_user_id).await?; + Contact::update_ask_for_friend_promotions(t, from_user_id).await?; } let type_kind = content_type_kind(&content); @@ -100,13 +53,13 @@ pub(crate) async fn handle_encrypted( tracing::info!("Handling incoming message: {type_kind}"); if let Some(request) = content.contact_request { - return contact::handle_contact_request(ctx, tr, from_user_id, request).await; + return contact::handle_contact_request(ctx, t, from_user_id, request).await; } if let Some(update) = content.contact_update { return contact::handle_contact_update( ctx, - tr, + t, from_user_id, content.sender_profile_counter, update, @@ -115,46 +68,42 @@ pub(crate) async fn handle_encrypted( } if let Some(update) = content.message_update { - return messages::handle_message_update(tr, from_user_id, update).await; + return text_message::handle_message_update(t, from_user_id, update).await; } if let Some(update) = content.media_update { - return media::handle_media_update(tr, from_user_id, update).await; + return media::handle_media_update(t, from_user_id, update).await; } if let Some(error) = content.error_messages { return errors::handle_error_message( - tr, + ctx, + t, from_user_id, - receipt_id, content.group_id.as_deref(), error, ) .await; } - if let Some(push_keys) = content.push_keys { - return pushkeys::handle_push_key(tr, from_user_id, push_keys).await; - } - if let Some(update) = content.user_discovery_update { - return user_discovery::handle_user_discovery_update(ctx, from_user_id, update).await; + return user_discovery::handle_user_discovery_update(ctx, t, from_user_id, update).await; } if let Some(request) = content.user_discovery_request { - return user_discovery::handle_user_discovery_request(ctx, tr, from_user_id, request).await; + return user_discovery::handle_user_discovery_request(ctx, t, from_user_id, request).await; } if let Some(proof) = content.key_verification_proof { - return verification::handle_key_verification_proof(tr, from_user_id, proof).await; + return verification::handle_key_verification_proof(t, from_user_id, proof).await; } if let Some(recovery) = content.passwordless_recovery { - return recovery::handle_passwordless_recovery(tr, from_user_id, recovery).await; + return recovery::handle_passwordless_recovery(t, from_user_id, recovery).await; } if let Some(heartbeat) = content.passwordless_recovery_heartbeat { - return recovery::handle_passwordless_recovery_heartbeat(tr, from_user_id, heartbeat).await; + return recovery::handle_passwordless_recovery_heartbeat(t, from_user_id, heartbeat).await; } let group_id = content @@ -162,11 +111,11 @@ pub(crate) async fn handle_encrypted( .ok_or_else(|| TwonlyError::Generic("group-scoped message has no group ID".into()))?; if let Some(create) = content.group_create { - return groups::handle_group_create(tr, from_user_id, &group_id, create).await; + return groups::handle_group_create(ctx, t, from_user_id, &group_id, create).await; } if let Some(join) = content.group_join { - return groups::handle_group_join(tr, from_user_id, &group_id, join).await; + return groups::handle_group_join(t, from_user_id, &group_id, join).await; } let is_member = sqlx::query_scalar!( @@ -174,7 +123,7 @@ pub(crate) async fn handle_encrypted( group_id, from_user_id, ) - .fetch_one(&mut **tr) + .fetch_one(&mut **t) .await? != 0; @@ -182,21 +131,21 @@ pub(crate) async fn handle_encrypted( let local_user_id = ctx.user_id().await?; if Group::direct_chat_id(local_user_id, from_user_id) == group_id { - let contact = Contact::get_contact_by_id(tr, from_user_id).await?; + let contact = Contact::get_contact_by_id(t, from_user_id).await?; if let Some(contact) = contact.filter(|value| value.accepted != 0 && value.deleted_by_user == 0) { - Group::create_direct_chat(ctx, tr, contact).await?; + Group::create_direct_chat(ctx, t, contact).await?; } else { sqlx::query!( "UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?", from_user_id, ) - .execute(&mut **tr) + .execute(&mut **t) .await?; messages::queue_encrypted_content( - tr, + t, from_user_id, proto::EncryptedContent { error_messages: Some(proto::encrypted_content::ErrorMessages { @@ -213,28 +162,28 @@ pub(crate) async fn handle_encrypted( } } - groups::ensure_group_member(tr, from_user_id, &group_id).await?; + groups::ensure_group_member(t, from_user_id, &group_id).await?; if content.resend_group_public_key.is_some() { - return groups::handle_resend_group_public_key(tr, from_user_id, &group_id).await; + return groups::handle_resend_group_public_key(t, from_user_id, &group_id).await; } if let Some(update) = content.group_update { - return groups::handle_group_update(tr, from_user_id, &group_id, update).await; + return groups::handle_group_update(ctx, t, from_user_id, &group_id, update).await; } if let Some(flame) = content.flame_sync { - return groups::handle_flame_sync(tr, &group_id, flame).await; + return groups::handle_flame_sync(t, &group_id, flame).await; } if let Some(message) = content.text_message { - return text_message::handle_text_message(tr, from_user_id, &group_id, message).await; + return text_message::handle_text_message(t, from_user_id, &group_id, message).await; } if let Some(message) = content.additional_data_message { return additional_data::handle_additional_data_message( ctx, - tr, + t, from_user_id, &group_id, message, @@ -243,15 +192,15 @@ pub(crate) async fn handle_encrypted( } if let Some(media) = content.media { - return media::handle_media(tr, from_user_id, &group_id, media).await; + return media::handle_media(t, from_user_id, &group_id, media).await; } if let Some(reaction) = content.reaction { - return reaction::handle_reaction(tr, from_user_id, &group_id, reaction).await; + return reaction::handle_reaction(t, from_user_id, &group_id, reaction).await; } if let Some(indicator) = content.typing_indicator { - return handle_typing_indicator(tr, from_user_id, &group_id, indicator).await; + return handle_typing_indicator(t, from_user_id, &group_id, indicator).await; } Err(TwonlyError::Generic(format!( diff --git a/rust/src/api/messages/incoming/client2client/pushkeys.rs b/rust/src/api/messages/incoming/client2client/pushkeys.rs deleted file mode 100644 index b31bcf6c..00000000 --- a/rust/src/api/messages/incoming/client2client/pushkeys.rs +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -use crate::api::proto::client::encrypted_content; -use crate::error::{Result, TwonlyError}; -use crate::utils::milliseconds_to_seconds; -use sqlx::{Sqlite, Transaction}; -use std::sync::atomic::{AtomicI64, Ordering}; - -static LAST_PUSH_KEY_REQUEST: AtomicI64 = AtomicI64::new(0); -pub(crate) async fn handle_push_key( - tx: &mut Transaction<'_, Sqlite>, - user: i64, - value: encrypted_content::PushKeys, -) -> Result<()> { - use encrypted_content::push_keys::Type; - match Type::try_from(value.r#type) - .map_err(|_| TwonlyError::Generic("invalid push-key message".into()))? - { - Type::Update => { - let (Some(id), Some(key), Some(created_at)) = - (value.key_id, value.key, value.created_at) - else { - return Err(TwonlyError::Generic("incomplete push-key update".into())); - }; - sqlx::query!( - r#" - INSERT INTO contact_push_keys(contact_id, key_id, key, created_at) - VALUES (?, ?, ?, ?) - ON CONFLICT(contact_id, key_id) - DO UPDATE SET key = excluded.key, created_at = excluded.created_at - "#, - user, - id, - key, - milliseconds_to_seconds(created_at), - ) - .execute(&mut **tx) - .await?; - Ok(()) - } - Type::Request => { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; - let previous = LAST_PUSH_KEY_REQUEST.load(Ordering::Relaxed); - if now - previous < 60 - || LAST_PUSH_KEY_REQUEST - .compare_exchange(previous, now, Ordering::AcqRel, Ordering::Relaxed) - .is_err() - { - return Ok(()); - } - if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { - tokio::spawn(async move { - (callbacks.api.push_key_requested)(user).await; - }); - } - Ok(()) - } - } -} diff --git a/rust/src/api/messages/incoming/client2client/reaction.rs b/rust/src/api/messages/incoming/client2client/reaction.rs index 90d7d0af..f87b7244 100644 --- a/rust/src/api/messages/incoming/client2client/reaction.rs +++ b/rust/src/api/messages/incoming/client2client/reaction.rs @@ -4,8 +4,7 @@ */ use crate::api::proto::client::encrypted_content; -use crate::bridge::callbacks::get_callbacks; -use crate::database::app::tables::Group; +use crate::database::app::tables::{Group, MediaFile, Receipt}; use crate::error::Result; use sqlx::{Sqlite, Transaction}; @@ -16,29 +15,20 @@ pub(crate) async fn handle_reaction( reaction: encrypted_content::Reaction, ) -> Result<()> { if reaction.remove { - sqlx::query!( - r#" - DELETE FROM reactions - WHERE message_id = ? AND sender_id = ? AND emoji = ? - "#, - reaction.target_message_id, + Receipt::delete_reaction( + t, + &reaction.target_message_id, from_user_id, - reaction.emoji, + &reaction.emoji, ) - .execute(&mut **t) .await?; } else { - sqlx::query!( - r#" - INSERT INTO reactions(message_id, emoji, sender_id) - VALUES (?, ?, ?) - ON CONFLICT(message_id, sender_id, emoji) DO NOTHING - "#, - reaction.target_message_id, - reaction.emoji, + Receipt::insert_reaction( + t, + &reaction.target_message_id, from_user_id, + &reaction.emoji, ) - .execute(&mut **t) .await?; } @@ -46,18 +36,7 @@ pub(crate) async fn handle_reaction( Group::increase_last_message_exchange_to_now(t, group_id).await?; } - if let Ok(callbacks) = get_callbacks() { - let message_id = reaction.target_message_id; - tokio::spawn(async move { - (callbacks.api.media_action)( - "response".into(), - String::new(), - from_user_id, - message_id, - ) - .await; - }); - } + MediaFile::handle_response_from_receiver(t, &reaction.target_message_id).await?; Ok(()) } diff --git a/rust/src/api/messages/incoming/client2client/recovery.rs b/rust/src/api/messages/incoming/client2client/recovery.rs index 3b643184..a0f7c552 100644 --- a/rust/src/api/messages/incoming/client2client/recovery.rs +++ b/rust/src/api/messages/incoming/client2client/recovery.rs @@ -3,13 +3,21 @@ * */ +use crate::api::messages::outgoing::send_c2c_message_to_contact; use crate::api::proto::client::encrypted_content; -use crate::error::Result; +use crate::api::proto::client::EncryptedContent; +use crate::api::Server; +use crate::bridge::api::ServerResult; +use crate::context::Context; +use crate::error::{twonly_error, Result}; +use crate::user_config::UserConfig; +use prost::Message as _; use sha2::{Digest, Sha256}; use sqlx::{Sqlite, Transaction}; +use std::sync::Arc; pub(crate) async fn handle_passwordless_recovery( - transaction: &mut Transaction<'_, Sqlite>, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, recovery: encrypted_content::PasswordLessRecovery, ) -> Result<()> { @@ -24,7 +32,7 @@ pub(crate) async fn handle_passwordless_recovery( "#, from_user_id, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; } else if let Some(share) = recovery.recovery_secret_share { sqlx::query!( @@ -39,19 +47,137 @@ pub(crate) async fn handle_passwordless_recovery( recovery.threshold, from_user_id, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; } - if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { + #[cfg(not(test))] + { + let ctx = Context::get_static()?.clone(); tokio::spawn(async move { - (callbacks.api.recovery_changed)().await; + if let Err(error) = perform_heartbeat(&ctx).await { + tracing::warn!(%error, "passwordless recovery heartbeat failed"); + } }); } Ok(()) } +pub(crate) async fn perform_heartbeat(ctx: &Arc) -> Result<()> { + let now = chrono::Utc::now(); + let base_config = UserConfig::load_required_from(ctx)?; + let mut config = base_config.clone(); + + if let Some(recovery) = config.password_less_recovery.as_mut() { + let server_due = recovery + .last_server_heartbeat + .is_none_or(|last| now.signed_duration_since(last).num_days() > 20); + if server_due { + if let Some(encrypted_key) = recovery.encrypted_server_key.clone() { + if let ServerResult::ErrorCode(_) = Server::register_passwordless_recovery( + ctx, + encrypted_key, + recovery.pin_unlock_token.clone(), + ) + .await? + { + return Err(twonly_error!("passwordless registration failed: {code}")); + } + recovery.last_server_heartbeat = Some(now); + } + } + + let contacts_due = recovery + .last_contact_heartbeat + .is_none_or(|last| now.signed_duration_since(last).num_hours() >= 24); + if contacts_due { + let database = ctx.get_app_database().await; + let contacts = sqlx::query!( + r#"SELECT user_id, recovery_secret_share FROM contacts + WHERE recovery_is_trusted_friend = 1 + AND recovery_last_heartbeat IS NULL + AND recovery_secret_share IS NOT NULL"# + ) + .fetch_all(&database.pool) + .await?; + drop(database); + + for contact in contacts { + let content = EncryptedContent { + passwordless_recovery: Some(encrypted_content::PasswordLessRecovery { + recovery_secret_share: contact.recovery_secret_share, + threshold: recovery.threshold, + delete: false, + }), + ..Default::default() + }; + send_c2c_message_to_contact() + .ctx(ctx) + .contact_id(contact.user_id) + .encrypted_content(content.encode_to_vec()) + .call() + .await?; + } + recovery.last_contact_heartbeat = Some(now); + } + } + + let database = ctx.get_app_database().await; + let contacts = sqlx::query!( + r#"SELECT user_id, recovery_contacts_secret_share FROM contacts + WHERE recovery_contacts_secret_share IS NOT NULL + AND (recovery_contacts_last_heartbeat IS NULL + OR recovery_contacts_last_heartbeat <= ?)"#, + (now - chrono::Duration::days(7)).timestamp(), + ) + .fetch_all(&database.pool) + .await?; + drop(database); + + for contact in contacts { + let Some(share) = contact.recovery_contacts_secret_share else { + continue; + }; + let content = EncryptedContent { + passwordless_recovery_heartbeat: Some( + encrypted_content::PasswordLessRecoveryHeartbeat { + hash: Sha256::digest(share).to_vec(), + }, + ), + ..Default::default() + }; + + send_c2c_message_to_contact() + .ctx(ctx) + .contact_id(contact.user_id) + .encrypted_content(content.encode_to_vec()) + .call() + .await?; + + let database = ctx.get_app_database().await; + sqlx::query!( + "UPDATE contacts SET recovery_contacts_last_heartbeat = ? WHERE user_id = ?", + now.timestamp(), + contact.user_id, + ) + .execute(&database.pool) + .await?; + } + + if config != base_config { + UserConfig::update_json( + ctx, + &serde_json::to_string(&base_config)?, + &serde_json::to_string(&config)?, + )?; + if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { + (callbacks.api.user_config_changed)(config).await; + } + } + Ok(()) +} + pub(crate) async fn handle_passwordless_recovery_heartbeat( - transaction: &mut Transaction<'_, Sqlite>, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, heartbeat: encrypted_content::PasswordLessRecoveryHeartbeat, ) -> Result<()> { @@ -63,15 +189,17 @@ pub(crate) async fn handle_passwordless_recovery_heartbeat( "#, from_user_id, ) - .fetch_optional(&mut **transaction) + .fetch_optional(&mut **t) .await? .flatten(); + let valid = share .as_deref() .is_some_and(|share| Sha256::digest(share).as_slice() == heartbeat.hash); + if share.is_none() { super::messages::queue_encrypted_content( - transaction, + t, from_user_id, crate::api::proto::client::EncryptedContent { passwordless_recovery: Some(encrypted_content::PasswordLessRecovery { @@ -97,7 +225,153 @@ pub(crate) async fn handle_passwordless_recovery_heartbeat( valid, from_user_id, ) - .execute(&mut **transaction) + .execute(&mut **t) .await?; + Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + async fn context() -> anyhow::Result<(tempfile::TempDir, Arc)> { + let temp = tempfile::tempdir()?; + let database_dir = temp.path().join("database"); + let data_dir = temp.path().join("data"); + std::fs::create_dir_all(data_dir.join("keyvalue"))?; + std::fs::write( + data_dir.join("keyvalue/user.json"), + serde_json::to_vec(&UserConfig::default())?, + )?; + let context = Context::init_for_testing(database_dir, data_dir).await?; + Ok((temp, context)) + } + + async fn insert_contact(ctx: &Arc, user_id: i64) -> Result<()> { + let database = ctx.get_app_database().await; + sqlx::query!( + "INSERT INTO contacts(user_id, username, accepted) VALUES (?, ?, 1)", + user_id, + format!("user_{user_id}"), + ) + .execute(&database.pool) + .await?; + Ok(()) + } + + #[tokio::test] + async fn recovery_share_is_stored_and_deleted() -> anyhow::Result<()> { + let (_temp, ctx) = context().await?; + insert_contact(&ctx, 7).await?; + let database = ctx.get_app_database().await; + + let mut transaction = database.pool.begin().await?; + handle_passwordless_recovery( + &mut transaction, + 7, + encrypted_content::PasswordLessRecovery { + recovery_secret_share: Some(vec![1, 2, 3]), + threshold: 2, + delete: false, + }, + ) + .await?; + transaction.commit().await?; + + let stored = sqlx::query!( + "SELECT recovery_contacts_secret_share, recovery_contacts_threshold FROM contacts WHERE user_id = 7" + ) + .fetch_one(&database.pool) + .await?; + assert_eq!(stored.recovery_contacts_secret_share, Some(vec![1, 2, 3])); + assert_eq!(stored.recovery_contacts_threshold, Some(2)); + + let mut transaction = database.pool.begin().await?; + handle_passwordless_recovery( + &mut transaction, + 7, + encrypted_content::PasswordLessRecovery { + delete: true, + ..Default::default() + }, + ) + .await?; + transaction.commit().await?; + + let deleted = sqlx::query!( + "SELECT recovery_contacts_secret_share, recovery_contacts_threshold FROM contacts WHERE user_id = 7" + ) + .fetch_one(&database.pool) + .await?; + assert_eq!(deleted.recovery_contacts_secret_share, None); + assert_eq!(deleted.recovery_contacts_threshold, None); + Ok(()) + } + + #[tokio::test] + async fn valid_and_invalid_heartbeat_update_the_expected_state() -> anyhow::Result<()> { + let (_temp, ctx) = context().await?; + insert_contact(&ctx, 8).await?; + let database = ctx.get_app_database().await; + let share = vec![4, 5, 6]; + sqlx::query!( + "UPDATE contacts SET recovery_secret_share = ? WHERE user_id = 8", + share, + ) + .execute(&database.pool) + .await?; + + let mut transaction = database.pool.begin().await?; + handle_passwordless_recovery_heartbeat( + &mut transaction, + 8, + encrypted_content::PasswordLessRecoveryHeartbeat { + hash: Sha256::digest([4, 5, 6]).to_vec(), + }, + ) + .await?; + transaction.commit().await?; + let valid = + sqlx::query_scalar!("SELECT recovery_last_heartbeat FROM contacts WHERE user_id = 8") + .fetch_one(&database.pool) + .await?; + assert!(valid.is_some()); + + let mut transaction = database.pool.begin().await?; + handle_passwordless_recovery_heartbeat( + &mut transaction, + 8, + encrypted_content::PasswordLessRecoveryHeartbeat { hash: vec![0; 32] }, + ) + .await?; + transaction.commit().await?; + let invalid = + sqlx::query_scalar!("SELECT recovery_last_heartbeat FROM contacts WHERE user_id = 8") + .fetch_one(&database.pool) + .await?; + assert_eq!(invalid, None); + Ok(()) + } + + #[tokio::test] + async fn heartbeat_without_share_queues_deletion_response() -> anyhow::Result<()> { + let (_temp, ctx) = context().await?; + insert_contact(&ctx, 9).await?; + let database = ctx.get_app_database().await; + let mut transaction = database.pool.begin().await?; + handle_passwordless_recovery_heartbeat( + &mut transaction, + 9, + encrypted_content::PasswordLessRecoveryHeartbeat { hash: vec![1; 32] }, + ) + .await?; + transaction.commit().await?; + + let queued = sqlx::query_scalar!("SELECT COUNT(*) FROM receipts WHERE contact_id = 9") + .fetch_one(&database.pool) + .await?; + assert_eq!(queued, 1); + Ok(()) + } +} diff --git a/rust/src/api/messages/incoming/client2client/text_message.rs b/rust/src/api/messages/incoming/client2client/text_message.rs index 5f1cfcfe..df21aaaf 100644 --- a/rust/src/api/messages/incoming/client2client/text_message.rs +++ b/rust/src/api/messages/incoming/client2client/text_message.rs @@ -7,6 +7,7 @@ use crate::api::proto::client::encrypted_content; use crate::database::app::tables::{Group, Message, MessageType, NewMessage}; use crate::error::Result; use crate::utils::milliseconds_to_seconds; +use encrypted_content::message_update::Type; use sqlx::{Sqlite, Transaction}; pub(crate) async fn handle_text_message( @@ -36,3 +37,162 @@ pub(crate) async fn handle_text_message( Ok(()) } + +pub(crate) async fn handle_message_update( + t: &mut Transaction<'_, Sqlite>, + from_user_id: i64, + update: encrypted_content::MessageUpdate, +) -> Result<()> { + let timestamp = milliseconds_to_seconds(update.timestamp); + + let update_type = Type::try_from(update.r#type)?; + tracing::info!(?update_type, from_user_id, "update text message"); + + match update_type { + Type::Opened => { + for message_id in update.multiple_target_message_ids { + let action_at = sqlx::query_scalar::<_, i64>( + "SELECT MAX(created_at, ?) FROM messages WHERE message_id = ?", + ) + .bind(timestamp) + .bind(&message_id) + .fetch_optional(&mut **t) + .await?; + + let Some(action_at) = action_at else { continue }; + + sqlx::query!( + r#" + INSERT INTO message_actions(message_id, contact_id, type, action_at) + VALUES (?, ?, 'openedAt', ?) + ON CONFLICT(message_id, contact_id, type) + DO UPDATE SET action_at = excluded.action_at + "#, + message_id, + from_user_id, + action_at, + ) + .execute(&mut **t) + .await?; + + sqlx::query!( + r#"UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS( + SELECT 1 FROM group_members gm + WHERE gm.group_id = messages.group_id AND NOT EXISTS( + SELECT 1 FROM message_actions ma + WHERE ma.message_id = messages.message_id + AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt' + ) + ) THEN ? ELSE NULL END + WHERE message_id = ?"#, + action_at, + action_at, + message_id, + ) + .execute(&mut **t) + .await?; + } + } + Type::Delete => { + let media_id = sqlx::query_scalar!( + "SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?", + update.sender_message_id, + from_user_id, + ) + .fetch_optional(&mut **t) + .await? + .flatten(); + + sqlx::query!( + "DELETE FROM message_histories WHERE message_id = ?", + update.sender_message_id + ) + .execute(&mut **t) + .await?; + + sqlx::query!( + "DELETE FROM receipts WHERE message_id = ?", + update.sender_message_id + ) + .execute(&mut **t) + .await?; + + sqlx::query!( + r#" + UPDATE messages + SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ? + WHERE message_id = ? AND sender_id = ? + "#, + timestamp, + update.sender_message_id, + from_user_id, + ) + .execute(&mut **t) + .await?; + + if let Some(media_id) = media_id { + let references = sqlx::query_scalar!( + "SELECT COUNT(*) FROM messages WHERE media_id = ?", + media_id, + ) + .fetch_one(&mut **t) + .await?; + + if references == 0 { + let media_type = sqlx::query_scalar!( + "SELECT type FROM media_files WHERE media_id = ?", + media_id, + ) + .fetch_optional(&mut **t) + .await?; + sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id) + .execute(&mut **t) + .await?; + if let Some(media_type) = media_type { + let ctx = crate::context::Context::get_static()?; + let ctx = ctx.clone(); + tokio::spawn(async move { + // Let the surrounding message transaction commit before + // applying its corresponding filesystem side effect. + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + if let Err(error) = + crate::services::mediafiles::MediaFileService::new(&ctx) + .remove_files_if_deleted(&media_id, &media_type) + .await + { + tracing::warn!(media_id, %error, "could not remove media files"); + } + }); + } + } + } + } + Type::EditText => { + sqlx::query!( + r#"INSERT INTO message_histories(message_id, content, created_at) + SELECT message_id, content, ? FROM messages + WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL"#, + timestamp, + update.sender_message_id, + from_user_id, + ) + .execute(&mut **t) + .await?; + + sqlx::query!( + r#" + UPDATE messages + SET content = ?, modified_at = ? + WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL + "#, + update.text, + timestamp, + update.sender_message_id, + from_user_id, + ) + .execute(&mut **t) + .await?; + } + } + Ok(()) +} diff --git a/rust/src/api/messages/incoming/client2client/user_discovery.rs b/rust/src/api/messages/incoming/client2client/user_discovery.rs index da8ad029..6780b7be 100644 --- a/rust/src/api/messages/incoming/client2client/user_discovery.rs +++ b/rust/src/api/messages/incoming/client2client/user_discovery.rs @@ -9,13 +9,10 @@ use crate::context::Context; use crate::database::app::tables::Contact; use crate::error::{twonly_error, Result, TwonlyError}; use crate::user_config::UserConfig; +use crate::user_discovery::UserDiscoveryVersion; +use prost::Message; use sqlx::{Sqlite, Transaction}; -use std::collections::HashSet; -use std::sync::LazyLock; -use tokio::sync::Mutex; - -static REQUESTED_UPDATES: LazyLock>> = - LazyLock::new(|| Mutex::new(HashSet::new())); +use std::sync::Arc; pub(crate) async fn check_sender_version( ctx: &Context, @@ -33,20 +30,30 @@ pub(crate) async fn check_sender_version( )); } - let Some(current_version) = ctx - .get_user_discovery() - .get() - .await - .should_request_new_messages(from_user_id, &version) - .await? - else { - return Ok(()); - }; + // The inbound message transaction owns the app database's sole connection. + // Going through `UserDiscovery::should_request_new_messages` here would ask + // the native store to acquire that same connection and deadlock until the + // pool's 30-second acquire timeout expires. + let received_version = UserDiscoveryVersion::decode(version.as_slice())?; + let stored_version = sqlx::query_scalar!( + "SELECT user_discovery_version FROM contacts WHERE user_id = ?", + from_user_id, + ) + .fetch_optional(&mut **t) + .await? + .flatten() + .map(|version| UserDiscoveryVersion::decode(version.as_slice())) + .transpose()? + .unwrap_or_default(); - if !REQUESTED_UPDATES.lock().await.insert(from_user_id) { + if received_version.announcement <= stored_version.announcement + && received_version.promotion <= stored_version.promotion + { return Ok(()); } + let current_version = stored_version.encode_to_vec(); + queue_encrypted_content( t, from_user_id, @@ -62,8 +69,9 @@ pub(crate) async fn check_sender_version( Ok(()) } + pub(crate) async fn handle_user_discovery_request( - ctx: &Context, + ctx: &Arc, t: &mut Transaction<'_, Sqlite>, from_user_id: i64, request: encrypted_content::UserDiscoveryRequest, @@ -82,7 +90,7 @@ pub(crate) async fn handle_user_discovery_request( .get_user_discovery() .get() .await - .get_new_messages(from_user_id, &request.current_version) + .get_new_messages(from_user_id, &request.current_version, t) .await?; if !messages.is_empty() { queue_encrypted_content( @@ -98,23 +106,27 @@ pub(crate) async fn handle_user_discovery_request( } Ok(()) } + pub(crate) async fn handle_user_discovery_update( - ctx: &Context, + ctx: &Arc, + t: &mut Transaction<'_, Sqlite>, from_user_id: i64, update: encrypted_content::UserDiscoveryUpdate, ) -> Result<()> { if !UserConfig::load_required_from(ctx)?.is_user_discovery_enabled { return Ok(()); } + if update.messages.iter().any(|message| message.is_empty()) { return Err(TwonlyError::Generic( "user-discovery update contains an empty message".into(), )); } + Ok(ctx .get_user_discovery() .get() .await - .handle_new_messages(from_user_id, None, update.messages) + .handle_new_messages(from_user_id, None, update.messages, t) .await?) } diff --git a/rust/src/api/messages/incoming/mod.rs b/rust/src/api/messages/incoming/mod.rs index 565008c4..d19bd77a 100644 --- a/rust/src/api/messages/incoming/mod.rs +++ b/rust/src/api/messages/incoming/mod.rs @@ -333,6 +333,8 @@ pub(crate) async fn handle_decoded_server_message( "messages", "groups", "contacts", + "key_verifications", + "user_discovery_own_promotions", ]); let ctx = ctx.clone(); diff --git a/rust/src/api/messages/outgoing.rs b/rust/src/api/messages/outgoing.rs index 5e261f9b..32324849 100644 --- a/rust/src/api/messages/outgoing.rs +++ b/rust/src/api/messages/outgoing.rs @@ -12,7 +12,7 @@ use crate::user_config::UserConfig; use prost::Message as _; use std::sync::Arc; -async fn decorate_content( +pub(crate) async fn decorate_content( ctx: &Context, contact_id: i64, content: &mut proto::EncryptedContent, @@ -35,6 +35,7 @@ async fn decorate_content( } if config.is_user_discovery_enabled && is_persisted_message { + ctx.initialize_user_discovery_from_config().await?; let database = ctx.get_app_database().await; let allowed = sqlx::query_scalar!( r#"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND accepted = 1 diff --git a/rust/src/api/runtime/auth.rs b/rust/src/api/runtime/auth.rs index 5c4a5984..fdd6fdb1 100644 --- a/rust/src/api/runtime/auth.rs +++ b/rust/src/api/runtime/auth.rs @@ -37,7 +37,7 @@ impl ApiAuthHandshaker { .ok_or_else(|| TwonlyError::Generic("User configuration not found".into()))?; let device_id = user.device_id; let app_version = user.app_version.to_string(); - Ok((user.user_id, device_id, app_version)) + Ok((Some(user.user_id), device_id, app_version)) } async fn request_handshake( diff --git a/rust/src/api/runtime/client.rs b/rust/src/api/runtime/client.rs index b3c4e26b..37b2b522 100644 --- a/rust/src/api/runtime/client.rs +++ b/rust/src/api/runtime/client.rs @@ -9,6 +9,7 @@ use crate::error::Result; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, LazyLock, Weak}; +use std::time::Duration; use stream_tungstenite::WebSocketClient; use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; @@ -90,6 +91,7 @@ impl ApiClient { }; let client = WebSocketClient::builder(host) + .receive_timeout(Duration::from_secs(60)) .handshaker(handshaker) .build(); @@ -155,8 +157,11 @@ impl ApiClient { pub async fn close(&self) { self.deliberately_closed.store(true, Ordering::Release); - if let Some(_) = self.ws_client.lock().await.take() { - // Drop client to disconnect + let client = self.ws_client.lock().await.take(); + if let Some(client) = client { + if let Err(error) = client.shutdown_graceful(Duration::from_secs(5)).await { + tracing::warn!("WebSocket shutdown did not finish cleanly: {error}"); + } } self.fail_pending().await; self.set_state(ApiConnectionState::Stopped).await; diff --git a/rust/src/api/server/purchases.rs b/rust/src/api/server/purchases.rs index ed8a03dc..d452154f 100644 --- a/rust/src/api/server/purchases.rs +++ b/rust/src/api/server/purchases.rs @@ -24,31 +24,8 @@ impl Server { .await } - pub async fn load_plan_balance(ctx: &Arc, use_cache: bool) -> Result> { - let database = ctx.get_app_database().await; - match Self::get_plan_balance(ctx).await { - Ok(response) => { - sqlx::query!( - r#" - INSERT INTO api_state(key, value) VALUES('plan_balance', ?) - ON CONFLICT(key) DO UPDATE SET value = excluded.value, - updated_at = CAST(strftime('%s', 'now') AS INTEGER) - "#, - response, - ) - .execute(&database.pool) - .await?; - database.notify_committed(["api_state"]); - Ok(response) - } - Err(network_error) if use_cache => { - sqlx::query_scalar!("SELECT value FROM api_state WHERE key = 'plan_balance'") - .fetch_optional(&database.pool) - .await? - .ok_or(network_error) - } - Err(error) => Err(error), - } + pub async fn load_plan_balance(ctx: &Arc) -> Result> { + Self::get_plan_balance(ctx).await } pub async fn ipa_purchase( diff --git a/rust/src/bridge/api.rs b/rust/src/bridge/api.rs index 20b1e7cf..77c1e21e 100644 --- a/rust/src/bridge/api.rs +++ b/rust/src/bridge/api.rs @@ -4,12 +4,15 @@ */ use crate::api::messages::incoming::client2client::messages; +use crate::api::proto::server_to_client::response::ok::Ok as ResponseOk; +use crate::api::runtime::helpers::decode_ok_value; use crate::api::ApiRuntime; pub use crate::api::PqcPreKeyInput; use crate::api::Server; use crate::context::Context; -use crate::error::Result; +use crate::error::{Result, TwonlyError}; use crate::frb_generated::StreamSink; +use crate::services::contacts::ContactService; use crate::user_config::UserConfig; use flutter_rust_bridge::frb; use prost::Message; @@ -20,49 +23,25 @@ pub enum ServerResult { ErrorCode(i32), } -#[frb] -pub enum ServerResultEmpty { - Ok, - ErrorCode(i32), -} - -#[frb] -pub enum ServerResultVecU8 { - Ok(Vec), - ErrorCode(i32), -} - -#[frb] -pub enum ServerResultI64 { - Ok(i64), - ErrorCode(i32), -} - -impl ServerResult<()> { - pub fn into_bridge(self) -> ServerResultEmpty { - match self { - ServerResult::Ok(()) => ServerResultEmpty::Ok, - ServerResult::ErrorCode(c) => ServerResultEmpty::ErrorCode(c), - } +fn api_result(result: ServerResult) -> Result { + match result { + ServerResult::Ok(value) => Ok(value), + ServerResult::ErrorCode(code) => Err(TwonlyError::Api(code)), } } -impl ServerResult> { - pub fn into_bridge(self) -> ServerResultVecU8 { - match self { - ServerResult::Ok(v) => ServerResultVecU8::Ok(v), - ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c), - } - } +fn empty_api_response(bytes: Vec) -> Result<()> { + api_result(decode_ok_value(bytes, |value| match value { + ResponseOk::None(_) => Some(()), + _ => None, + })?) } -impl ServerResult { - pub fn into_bridge(self) -> ServerResultI64 { - match self { - ServerResult::Ok(v) => ServerResultI64::Ok(v), - ServerResult::ErrorCode(c) => ServerResultI64::ErrorCode(c), - } - } +fn encoded_api_response( + bytes: Vec, + extract: impl FnOnce(ResponseOk) -> Option, +) -> Result> { + api_result(decode_ok_value(bytes, extract)?).map(|value| value.encode_to_vec()) } #[derive(Clone, Debug)] @@ -133,7 +112,7 @@ pub struct PreparedOutgoingMessage { impl RustApi { pub async fn request_contact_by_username(username: String) -> Result<()> { let ctx = Context::get_static()?; - crate::services::contacts::ContactService::new(ctx) + ContactService::new(ctx) .request_by_username(username, false) .await } @@ -206,122 +185,156 @@ impl RustApi { proof_of_work: i64, lang_code: String, is_ios: bool, - ) -> Result { + ) -> Result { let ctx = Context::get_static()?; Server::register(ctx, username, proof_of_work, lang_code, is_ios) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } - pub async fn get_user_by_id(user_id: i64) -> Result { + pub async fn get_user_by_id(user_id: i64) -> Result> { let ctx = Context::get_static()?; Server::get_user_by_id(ctx, user_id) .await - .map(|r| r.into_bridge()) + .and_then(api_result) + .map(|value| value.encode_to_vec()) } pub async fn check_for_deleted_usernames() -> Result<()> { let ctx = Context::get_static()?; Server::check_for_deleted_usernames(ctx).await } - pub async fn get_user_id_from_username(username: String) -> Result { + pub async fn get_user_id_from_username(username: String) -> Result { let ctx = Context::get_static()?; Server::get_user_id_from_username(ctx, username) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } - pub async fn get_user_data(username: String) -> Result { + pub async fn get_user_data(username: String) -> Result> { let ctx = Context::get_static()?; Server::get_user_by_username(ctx, username) .await - .map(|r| r.into_bridge()) + .and_then(api_result) + .map(|value| value.encode_to_vec()) } - pub async fn get_proof_of_work() -> Result { + pub async fn get_proof_of_work() -> Result> { let ctx = Context::get_static()?; - Server::get_proof_of_work(ctx).await.map(|r| match r { - ServerResult::Ok(data) => ServerResultVecU8::Ok(data.encode_to_vec()), - ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c), - }) - } - pub async fn download_done(token: Vec) -> Result { - let ctx = Context::get_static()?; - Server::download_done(ctx, token) + Server::get_proof_of_work(ctx) .await - .map(|r| r.into_bridge()) + .and_then(api_result) + .map(|value| value.encode_to_vec()) } - pub async fn set_login_token(token: Vec) -> Result { + pub async fn download_done(token: Vec) -> Result<()> { + let ctx = Context::get_static()?; + Server::download_done(ctx, token).await.and_then(api_result) + } + + pub async fn download_media(media_id: String) -> Result<()> { + let ctx = Context::get_static()?; + crate::services::mediafiles::MediaFileService::new(ctx) + .download_when_available(&media_id) + .await + } + + pub async fn download_pending_media() -> Result<()> { + let ctx = Context::get_static()?; + crate::services::mediafiles::MediaFileService::new(ctx) + .download_pending() + .await + } + + pub async fn request_media_reupload(media_id: String) -> Result<()> { + let ctx = Context::get_static()?; + crate::services::mediafiles::MediaFileService::new(ctx) + .request_reupload(&media_id) + .await + } + pub async fn set_login_token(token: Vec) -> Result<()> { let ctx = Context::get_static()?; Server::set_login_token(ctx, token) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } pub async fn request_memories_upload( size: i64, original_date: i64, media_id: String, - ) -> Result { + ) -> Result> { let ctx = Context::get_static()?; Server::request_memories_upload(ctx, size, original_date, media_id) .await - .map(|r| r.into_bridge()) + .and_then(api_result) + .map(|value| value.encode_to_vec()) } - pub async fn get_memories_usage() -> Result { + pub async fn get_memories_usage() -> Result> { let ctx = Context::get_static()?; Server::get_memories_usage(ctx) .await - .map(|r| r.into_bridge()) + .and_then(api_result) + .map(|value| value.encode_to_vec()) } - pub async fn get_memories_url(media_id: String, thumbnail: bool) -> Result { + pub async fn get_memories_url(media_id: String, thumbnail: bool) -> Result> { let ctx = Context::get_static()?; Server::get_memories_url(ctx, media_id, thumbnail) .await - .map(|r| r.into_bridge()) + .and_then(api_result) + .map(|value| value.encode_to_vec()) } - pub async fn confirm_memories_upload(media_id: String) -> Result { + pub async fn confirm_memories_upload(media_id: String) -> Result<()> { let ctx = Context::get_static()?; Server::confirm_memories_upload(ctx, media_id) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } - pub async fn delete_memory(media_id: String) -> Result { + pub async fn delete_memory(media_id: String) -> Result<()> { let ctx = Context::get_static()?; Server::delete_memory(ctx, media_id) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } - pub async fn disable_memories_backup() -> Result { + pub async fn disable_memories_backup() -> Result<()> { let ctx = Context::get_static()?; Server::disable_memories_backup(ctx) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } pub async fn get_plan_balance() -> Result> { let ctx = Context::get_static()?; - Server::get_plan_balance(ctx).await + encoded_api_response(Server::get_plan_balance(ctx).await?, |value| match value { + ResponseOk::Planballance(balance) => Some(balance), + _ => None, + }) } - pub async fn load_plan_balance(use_cache: bool) -> Result> { + pub async fn load_plan_balance() -> Result> { let ctx = Context::get_static()?; - Server::load_plan_balance(ctx, use_cache).await + encoded_api_response(Server::load_plan_balance(ctx).await?, |value| match value { + ResponseOk::Planballance(balance) => Some(balance), + _ => None, + }) } - pub async fn remove_additional_user(user_id: i64) -> Result { + pub async fn remove_additional_user(user_id: i64) -> Result<()> { let ctx = Context::get_static()?; Server::remove_additional_user(ctx, user_id) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } - pub async fn add_additional_user(user_id: i64) -> Result { + pub async fn add_additional_user(user_id: i64) -> Result<()> { let ctx = Context::get_static()?; Server::add_additional_user(ctx, user_id) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } pub async fn register_passwordless_recovery( encrypted_server_key: Vec, pin_unlock_token: Option>, - ) -> Result { + ) -> Result<()> { let ctx = Context::get_static()?; Server::register_passwordless_recovery(ctx, encrypted_server_key, pin_unlock_token) .await - .map(|r| r.into_bridge()) + .and_then(api_result) + } + pub async fn perform_passwordless_recovery_heartbeat() -> Result<()> { + let ctx = Context::get_static()?; + crate::api::messages::incoming::client2client::recovery::perform_heartbeat(ctx).await } pub async fn get_server_key_for_passwordless_recovery( user_id: i64, @@ -329,7 +342,7 @@ impl RustApi { pin_unlock_token: Option>, pin_protection_key: Option>, email: Option, - ) -> Result { + ) -> Result> { let ctx = Context::get_static()?; Server::get_server_key_for_passwordless_recovery( ctx, @@ -340,23 +353,23 @@ impl RustApi { email, ) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } pub async fn submit_recovery_share( notification_id: String, encrypted_message: Vec, - ) -> Result { + ) -> Result<()> { let ctx = Context::get_static()?; Server::submit_recovery_share(ctx, notification_id, encrypted_message) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } pub async fn register_passwordless_notification( notification_id: String, download_auth_token: Vec, lang_code: String, google_fcm: Option, - ) -> Result { + ) -> Result<()> { let ctx = Context::get_static()?; Server::register_passwordless_notification( ctx, @@ -366,13 +379,13 @@ impl RustApi { google_fcm, ) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } pub async fn check_for_passwordless_notification( notification_id: String, download_auth_token: Vec, already_received_message_ids: Vec, - ) -> Result { + ) -> Result> { let ctx = Context::get_static()?; Server::check_for_passwordless_notification( ctx, @@ -381,49 +394,46 @@ impl RustApi { already_received_message_ids, ) .await - .map(|r| r.into_bridge()) + .and_then(api_result) + .map(|value| value.encode_to_vec()) } - pub async fn report_user(user_id: i64, reason: String) -> Result { + pub async fn report_user(user_id: i64, reason: String) -> Result<()> { let ctx = Context::get_static()?; Server::report_user(ctx, user_id, reason) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } - pub async fn delete_account() -> Result { + pub async fn delete_account() -> Result<()> { let ctx = Context::get_static()?; - Server::delete_account(ctx).await.map(|r| r.into_bridge()) + Server::delete_account(ctx).await.and_then(api_result) } - pub async fn update_fcm_token(token: String) -> Result { + pub async fn update_fcm_token(token: String) -> Result<()> { let ctx = Context::get_static()?; Server::update_fcm_token(ctx, token) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } pub async fn ipa_purchase( product_id: String, source: String, verification_data: String, - ) -> Result> { + ) -> Result<()> { let ctx = Context::get_static()?; - Server::ipa_purchase(ctx, product_id, source, verification_data).await + empty_api_response(Server::ipa_purchase(ctx, product_id, source, verification_data).await?) } - pub async fn change_username(username: String) -> Result { + pub async fn change_username(username: String) -> Result<()> { let ctx = Context::get_static()?; Server::change_username(ctx, username) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } - pub async fn force_ipa_check() -> Result { + pub async fn force_ipa_check() -> Result<()> { let ctx = Context::get_static()?; - Server::force_ipa_check(ctx).await.map(|r| r.into_bridge()) + Server::force_ipa_check(ctx).await.and_then(api_result) } - pub async fn update_signed_pre_key( - id: i64, - key: Vec, - signature: Vec, - ) -> Result> { + pub async fn update_signed_pre_key(id: i64, key: Vec, signature: Vec) -> Result<()> { let ctx = Context::get_static()?; - Server::update_signed_pre_key(ctx, id, key, signature).await + empty_api_response(Server::update_signed_pre_key(ctx, id, key, signature).await?) } #[allow(clippy::too_many_arguments)] pub async fn upload_pqc_pre_keys( @@ -434,7 +444,7 @@ impl RustApi { kyber_signed_prekey: Vec, kyber_signed_prekey_signature: Vec, prekeys: Vec, - ) -> Result> { + ) -> Result<()> { let ctx = Context::get_static()?; Server::upload_pqc_pre_keys( ctx, @@ -447,16 +457,17 @@ impl RustApi { prekeys, ) .await + .and_then(empty_api_response) } pub async fn send_text_message( user_id: i64, body: Vec, push_data: Option>, - ) -> Result { + ) -> Result<()> { let ctx = Context::get_static()?; Server::send_text_message(ctx, user_id, body, push_data) .await - .map(|r| r.into_bridge()) + .and_then(api_result) } pub async fn send_encrypted_content( @@ -587,48 +598,3 @@ impl RustApi { .await } } - -impl ServerResult { - pub fn into_bridge(self) -> ServerResultVecU8 { - match self { - ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)), - ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c), - } - } -} - -impl ServerResult { - pub fn into_bridge(self) -> ServerResultVecU8 { - match self { - ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)), - ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c), - } - } -} - -impl ServerResult { - pub fn into_bridge(self) -> ServerResultVecU8 { - match self { - ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)), - ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c), - } - } -} - -impl ServerResult { - pub fn into_bridge(self) -> ServerResultVecU8 { - match self { - ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)), - ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c), - } - } -} - -impl ServerResult { - pub fn into_bridge(self) -> ServerResultVecU8 { - match self { - ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)), - ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c), - } - } -} diff --git a/rust/src/bridge/callbacks.rs b/rust/src/bridge/callbacks.rs index 7a381d72..704a935a 100644 --- a/rust/src/bridge/callbacks.rs +++ b/rust/src/bridge/callbacks.rs @@ -54,15 +54,11 @@ callback_generator! { }, Api api { resync_signal_session: (i64) => (), - push_key_requested: (i64) => (), - group_membership_error: (i64, String, String) => (), media_action: (String, String, i64, String) => (), verification_proof: (i64, Vec) => (), - create_push_data: (i64, Option, Vec, i32) => Option>, create_push_avatars: (i64) => (), - recovery_changed: () => (), media_received: (String, i64) => (), - group_state_refresh: (String, bool) => () + user_config_changed: (crate::user_config::UserConfig) => () } } } diff --git a/rust/src/bridge/mod.rs b/rust/src/bridge/mod.rs index 9f243550..9ac1bba8 100644 --- a/rust/src/bridge/mod.rs +++ b/rust/src/bridge/mod.rs @@ -7,6 +7,7 @@ pub mod api; pub mod callbacks; pub mod groups; +pub mod user_config; pub mod wrapper; use std::sync::Arc; @@ -20,13 +21,12 @@ use crate::error::TwonlyError; use crate::keys::KeyManager; use crate::secure_storage::SecureStorage; use crate::signal::engine::RustSignalEngine; -use crate::user_discovery::stores::{NativeUserDiscoveryStore, NativeUserDiscoveryUtils}; use crate::user_discovery::UserDiscovery; use crate::utils::Shared; use flutter_rust_bridge::frb; -pub use crate::user_discovery::traits::AnnouncedUser; -pub use crate::user_discovery::traits::OtherPromotion; +pub use crate::user_discovery::AnnouncedUser; +pub use crate::user_discovery::OtherPromotion; use tokio::sync::{Mutex, OnceCell, RwLock}; pub struct InitConfig { @@ -54,8 +54,7 @@ pub struct _AnnouncedUser { pub(crate) struct TwonlyFlutter { #[allow(dead_code)] pub(crate) config: InitConfig, - pub(crate) user_discovery: - Shared>, + pub(crate) user_discovery: Shared, #[allow(dead_code)] pub(crate) rust_db: Arc>>, pub(crate) app_db: Arc>>, diff --git a/rust/src/bridge/user_config.rs b/rust/src/bridge/user_config.rs new file mode 100644 index 00000000..f434ff90 --- /dev/null +++ b/rust/src/bridge/user_config.rs @@ -0,0 +1,70 @@ +use crate::context::Context; +use crate::error::Result; +use crate::user_config::UserConfig; + +pub struct UserConfigApi {} + +impl UserConfigApi { + pub async fn load() -> Result> { + let ctx = Context::get_static()?; + UserConfig::load_from(ctx) + } + + pub async fn create( + user_id: i64, + username: String, + display_name: String, + current_setup_page: Option, + app_version: i64, + ) -> Result { + serde_json::from_value(serde_json::json!({ + "userId": user_id, + "username": username, + "displayName": display_name, + "subscriptionPlan": "Free", + "currentSetupPage": current_setup_page, + "appVersion": app_version, + })) + .map_err(Into::into) + } + + #[flutter_rust_bridge::frb(sync)] + pub fn clone(config: UserConfig) -> UserConfig { + config + } + + pub async fn save(config: UserConfig) -> Result { + let ctx = Context::get_static()?; + let normalized = UserConfig::save_json(ctx, &serde_json::to_string(&config)?)?; + let config: UserConfig = serde_json::from_str(&normalized)?; + let mut key_manager = ctx.get_key_manager().await?; + if key_manager.user_id != Some(config.user_id) { + key_manager.user_id = Some(config.user_id); + key_manager.store_to_keychain(ctx.get_secure_storage())?; + } + drop(key_manager); + if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { + (callbacks.api.user_config_changed)(config.clone()).await; + } + Ok(config) + } + + pub async fn update(base: UserConfig, config: UserConfig) -> Result { + let ctx = Context::get_static()?; + let normalized = UserConfig::update_json( + ctx, + &serde_json::to_string(&base)?, + &serde_json::to_string(&config)?, + )?; + let config: UserConfig = serde_json::from_str(&normalized)?; + if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { + (callbacks.api.user_config_changed)(config.clone()).await; + } + Ok(config) + } + + pub async fn import_json(json: String) -> Result { + let config: UserConfig = serde_json::from_str(&json)?; + Self::save(config).await + } +} diff --git a/rust/src/bridge/wrapper/user_discovery.rs b/rust/src/bridge/wrapper/user_discovery.rs index 1bb48190..b0afc93c 100644 --- a/rust/src/bridge/wrapper/user_discovery.rs +++ b/rust/src/bridge/wrapper/user_discovery.rs @@ -3,36 +3,12 @@ * */ -use crate::bridge::callbacks::CURRENT_CALLBACK_ID; -use crate::bridge::get_twonly_flutter; -use crate::error::Result; +use crate::{bridge::callbacks::CURRENT_CALLBACK_ID, bridge::get_twonly_flutter, error::Result}; pub struct FlutterUserDiscovery {} impl FlutterUserDiscovery { - pub async fn initialize_or_update( - callback_id: u32, - threshold: u8, - user_id: i64, - public_key: Vec, - share_promotion: bool, - ) -> Result<()> { - CURRENT_CALLBACK_ID - .scope(callback_id, async move { - tracing::info!("Rust bridge: initialize_or_update started"); - let twonly = get_twonly_flutter()?; - tracing::info!("Rust bridge: getting user_discovery lock"); - let user_discovery = twonly.user_discovery.get().await; - tracing::info!("Rust bridge: calling initialize_or_update on protocols"); - let res = user_discovery - .initialize_or_update(threshold, user_id, public_key, share_promotion) - .await; - tracing::info!("Rust bridge: initialize_or_update on protocols finished"); - Ok(res?) - }) - .await - } - + /// UI-facing read used to display the current discovery version. pub async fn get_current_version(callback_id: u32) -> Result> { CURRENT_CALLBACK_ID .scope(callback_id, async move { @@ -46,58 +22,7 @@ impl FlutterUserDiscovery { .await } - pub async fn get_new_messages( - callback_id: u32, - contact_id: i64, - received_version: &[u8], - ) -> Result>> { - CURRENT_CALLBACK_ID - .scope(callback_id, async move { - Ok(get_twonly_flutter()? - .user_discovery - .get() - .await - .get_new_messages(contact_id, received_version) - .await?) - }) - .await - } - - pub async fn should_request_new_messages( - callback_id: u32, - contact_id: i64, - version: &[u8], - ) -> Result>> { - CURRENT_CALLBACK_ID - .scope(callback_id, async move { - Ok(get_twonly_flutter()? - .user_discovery - .get() - .await - .should_request_new_messages(contact_id, version) - .await?) - }) - .await - } - - pub async fn handle_new_messages( - callback_id: u32, - contact_id: i64, - public_key_verified_timestamp: Option, - messages: Vec>, - ) -> Result<()> { - CURRENT_CALLBACK_ID - .scope(callback_id, async move { - Ok(get_twonly_flutter()? - .user_discovery - .get() - .await - .handle_new_messages(contact_id, public_key_verified_timestamp, messages) - .await?) - }) - .await - } - + /// UI-facing hook used when a user manually changes contact verification. pub async fn update_verification_state_for_user( callback_id: u32, contact_id: i64, @@ -105,12 +30,54 @@ impl FlutterUserDiscovery { ) -> Result<()> { CURRENT_CALLBACK_ID .scope(callback_id, async move { - Ok(get_twonly_flutter()? - .user_discovery + let ctx = get_twonly_flutter()?; + let database = ctx.app_db.read().await.clone(); + let mut transaction = database.pool.begin().await?; + ctx.user_discovery .get() .await - .update_verification_state_for_user(contact_id, public_key_verified_timestamp) - .await?) + .update_verification_state_for_user( + contact_id, + public_key_verified_timestamp, + &mut transaction, + ) + .await?; + transaction.commit().await?; + Ok(()) + }) + .await + } + + pub async fn change_exclusion_for_contact( + callback_id: u32, + contact_id: i64, + exclude: bool, + ) -> Result<()> { + CURRENT_CALLBACK_ID + .scope(callback_id, async move { + let ctx = get_twonly_flutter()?; + let database = ctx.app_db.read().await.clone(); + let mut transaction = database.pool.begin().await?; + sqlx::query!( + "UPDATE user_discovery_own_promotions SET promotion = X'' WHERE contact_id = ?", + contact_id, + ) + .execute(&mut *transaction) + .await?; + sqlx::query!( + r#" + UPDATE contacts + SET user_discovery_excluded = ?, user_discovery_version = NULL + WHERE user_id = ? + "#, + exclude, + contact_id, + ) + .execute(&mut *transaction) + .await?; + transaction.commit().await?; + database.notify_committed(["user_discovery_own_promotions", "contacts"]); + Ok(()) }) .await } diff --git a/rust/src/context.rs b/rust/src/context.rs index 9d8d4916..fec5ecbf 100644 --- a/rust/src/context.rs +++ b/rust/src/context.rs @@ -14,11 +14,11 @@ use crate::keys::KeyManager; #[cfg(not(test))] use crate::log::init_tracing; use crate::signal::engine::RustSignalEngine; -use crate::user_discovery::stores::{NativeUserDiscoveryStore, NativeUserDiscoveryUtils}; use crate::user_discovery::UserDiscovery; use crate::utils::Shared; use crate::{bridge::TwonlyFlutter, secure_storage::SecureStorage}; use libsignal_protocol::IdentityKey; +use libsignal_protocol::IdentityKeyPair; use std::{path::PathBuf, sync::Arc}; use tokio::sync::{Mutex, OnceCell, RwLock}; #[cfg(not(test))] @@ -36,8 +36,7 @@ pub struct TwonlyStandalone { #[allow(dead_code)] pub(crate) secure_storage: SecureStorage, pub(crate) key_manager: Arc>, - pub(crate) user_discovery: - Shared>, + pub(crate) user_discovery: Shared, pub(crate) signal_engine: Arc>>, pub(crate) api_client: OnceCell>>, } @@ -63,9 +62,7 @@ impl Context { } } - pub(crate) fn get_user_discovery( - &self, - ) -> &Shared> { + pub(crate) fn get_user_discovery(&self) -> &Shared { match self { Self::Flutter(value) => &value.user_discovery, Self::Standalone(value) => &value.user_discovery, @@ -133,8 +130,9 @@ impl Context { let rust_db = Arc::new(RwLock::new(rust_db)); let key_manager = Arc::new(Mutex::new(key_manager)); let user_discovery = Shared::new(UserDiscovery::new( - NativeUserDiscoveryStore::new(app_db.clone(), data_dir.to_str().unwrap()), - NativeUserDiscoveryUtils::new(key_manager.clone(), rust_db.clone()), + data_dir.to_str().unwrap(), + key_manager.clone(), + rust_db.clone(), )?); let ctx = Arc::new(Context::from_standalone(TwonlyStandalone { @@ -192,6 +190,73 @@ impl Context { user_id.to_string(), )?); } + self.initialize_user_discovery_from_config().await?; + Ok(()) + } + + pub(crate) async fn initialize_user_discovery_from_config(&self) -> Result<()> { + let Some(config) = crate::user_config::UserConfig::load_from(self)? else { + return Ok(()); + }; + if !config.is_user_discovery_enabled { + return Ok(()); + } + + let discovery_config_path = + PathBuf::from(self.data_dir()).join("user_discovery_config.json"); + let settings_are_current = std::fs::read_to_string(&discovery_config_path) + .ok() + .and_then(|value| serde_json::from_str::(&value).ok()) + .is_some_and(|value| { + value.get("threshold").and_then(serde_json::Value::as_u64) + == Some(u64::from(config.user_discovery_threshold)) + && value + .get("share_promotion") + .and_then(serde_json::Value::as_bool) + == Some(config.user_discovery_share_promotion) + }); + if settings_are_current { + let database = self.get_app_database().await; + let has_shares = + sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM user_discovery_shares LIMIT 1)") + .fetch_one(&database.pool) + .await? + != 0; + if has_shares { + return Ok(()); + } + } + + let key_manager = self.get_key_manager().await?; + let user_id = key_manager.user_id.ok_or_else(|| { + TwonlyError::Generic("cannot initialize user discovery without user ID".into()) + })?; + let identity = key_manager + .signal_identity + .as_ref() + .ok_or(TwonlyError::SignalIdentityNotFound)?; + let identity = IdentityKeyPair::try_from(identity.identity_key_pair_structure.as_slice()) + .map_err(|error| { + TwonlyError::Generic(format!("invalid Signal identity: {error}")) + })?; + let public_key = identity.identity_key().serialize().to_vec(); + drop(key_manager); + + let database = self.get_app_database().await; + let mut transaction = database.pool.begin().await?; + self.get_user_discovery() + .get() + .await + .initialize_or_update( + config.user_discovery_threshold, + user_id, + public_key, + config.user_discovery_share_promotion, + &mut transaction, + ) + .await?; + transaction.commit().await?; + database.notify_committed(["user_discovery_shares"]); Ok(()) } @@ -281,8 +346,9 @@ impl Context { Arc::new(Mutex::new(engine)) }; let user_discovery = Shared::new(UserDiscovery::new( - NativeUserDiscoveryStore::new(app_db.clone(), &config.data_dir), - NativeUserDiscoveryUtils::new(key_manager.clone(), rust_db_handle.clone()), + &config.data_dir, + key_manager.clone(), + rust_db_handle.clone(), )?); let ctx = Arc::new(Context::Flutter(TwonlyFlutter { config, @@ -294,6 +360,9 @@ impl Context { signal_engine, api_client: OnceCell::const_new(), })); + if let Err(error) = ctx.initialize_user_discovery_from_config().await { + tracing::warn!("failed to initialize user discovery: {error}"); + } ApiRuntime::initialize(&ctx).await?; Ok(ctx) } else { @@ -312,8 +381,9 @@ impl Context { Arc::new(Mutex::new(engine)) }; let user_discovery = Shared::new(UserDiscovery::new( - NativeUserDiscoveryStore::new(app_db.clone(), &config.data_dir), - NativeUserDiscoveryUtils::new(key_manager.clone(), rust_db_handle.clone()), + &config.data_dir, + key_manager.clone(), + rust_db_handle.clone(), )?); let ctx = Arc::new(Context::Standalone(TwonlyStandalone { config, @@ -325,6 +395,9 @@ impl Context { signal_engine, api_client: OnceCell::const_new(), })); + if let Err(error) = ctx.initialize_user_discovery_from_config().await { + tracing::warn!("failed to initialize user discovery: {error}"); + } ApiRuntime::initialize(&ctx).await?; Ok(ctx) } diff --git a/rust/src/database/app/migrations/0002_api_outbox.sql b/rust/src/database/app/migrations/0002_api_outbox.sql new file mode 100644 index 00000000..2f161337 --- /dev/null +++ b/rust/src/database/app/migrations/0002_api_outbox.sql @@ -0,0 +1,16 @@ +CREATE TABLE + api_outbox ( + sequence_id INTEGER NOT NULL PRIMARY KEY, + payload BLOB NOT NULL, + operation_kind TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT ( + CAST(strftime ('%s', CURRENT_TIMESTAMP) AS INTEGER) + ), + attempt_count INTEGER NOT NULL DEFAULT 0, + next_attempt_at INTEGER NOT NULL DEFAULT ( + CAST(strftime ('%s', CURRENT_TIMESTAMP) AS INTEGER) + ), + expires_at INTEGER + ); + +CREATE INDEX idx_api_outbox_next_attempt ON api_outbox (next_attempt_at); diff --git a/rust/src/database/app/migrations/0002_api_state.sql b/rust/src/database/app/migrations/0002_api_state.sql deleted file mode 100644 index 6644bb8b..00000000 --- a/rust/src/database/app/migrations/0002_api_state.sql +++ /dev/null @@ -1,25 +0,0 @@ --- State required by the standalone Rust API and client-to-client processor. -CREATE TABLE api_state ( - key TEXT NOT NULL PRIMARY KEY, - value BLOB NOT NULL, - updated_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)) -); - -CREATE TABLE api_outbox ( - sequence_id INTEGER NOT NULL PRIMARY KEY, - payload BLOB NOT NULL, - operation_kind TEXT NOT NULL, - created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), - attempt_count INTEGER NOT NULL DEFAULT 0, - next_attempt_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', CURRENT_TIMESTAMP) AS INTEGER)), - expires_at INTEGER -); -CREATE INDEX idx_api_outbox_next_attempt ON api_outbox(next_attempt_at); - -CREATE TABLE contact_push_keys ( - contact_id INTEGER NOT NULL REFERENCES contacts(user_id) ON DELETE CASCADE, - key_id INTEGER NOT NULL, - key BLOB NOT NULL, - created_at INTEGER NOT NULL, - PRIMARY KEY (contact_id, key_id) -); diff --git a/rust/src/database/app/tables/contact.rs b/rust/src/database/app/tables/contact.rs index 77796ed4..0698e9c5 100644 --- a/rust/src/database/app/tables/contact.rs +++ b/rust/src/database/app/tables/contact.rs @@ -3,7 +3,10 @@ * */ -use crate::{context::Context, error::Result}; +use crate::{ + context::Context, + error::{Result, TwonlyError}, +}; use sqlx::{Sqlite, Transaction}; #[derive(sqlx::FromRow, Debug, Clone)] @@ -119,7 +122,7 @@ impl Contact { sqlx::query!( r#" INSERT INTO contacts(user_id, username, signal_version, accepted, requested, deleted_by_user, blocked) - VALUES (?, COALESCE(?, '[Unknown]'), COALESCE(?, 'v1'), COALESCE(?, 0), COALESCE(?, 0), COALESCE(?, 0), COALESCE(?, 0)) + VALUES (?, COALESCE(?, '[Unknown]'), COALESCE(?, 'v2'), COALESCE(?, 0), COALESCE(?, 0), COALESCE(?, 0), COALESCE(?, 0)) ON CONFLICT(user_id) DO UPDATE SET username = COALESCE(?, contacts.username), signal_version = COALESCE(?, contacts.signal_version), @@ -212,4 +215,17 @@ impl Contact { != 0; Ok(exists) } + + pub async fn ensure_exists( + transaction: &mut Transaction<'_, Sqlite>, + user_id: i64, + ) -> Result<()> { + if !Self::exists(transaction, user_id).await? { + return Err(TwonlyError::Generic(format!( + "contact {user_id} does not exist" + ))); + } + + Ok(()) + } } diff --git a/rust/src/database/app/tables/groups.rs b/rust/src/database/app/tables/groups.rs index f058c545..164dc2ef 100644 --- a/rust/src/database/app/tables/groups.rs +++ b/rust/src/database/app/tables/groups.rs @@ -15,6 +15,24 @@ const MAX_FUTURE_TIMESTAMP_SKEW_SECONDS: i64 = 10 * 60; pub struct Group; impl Group { + pub async fn ensure_exists(tr: &mut Transaction<'_, Sqlite>, group_id: &str) -> Result<()> { + let exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM groups WHERE group_id = ?)", + group_id + ) + .fetch_one(&mut **tr) + .await? + != 0; + + if !exists { + return Err(TwonlyError::Generic(format!( + "group {group_id} does not exist" + ))); + } + + Ok(()) + } + pub async fn create_direct_chat( ctx: &Context, tr: &mut Transaction<'_, Sqlite>, @@ -65,6 +83,40 @@ impl Group { ) } + pub async fn is_direct_chat(tr: &mut Transaction<'_, Sqlite>, group_id: &str) -> Result { + let is_direct = sqlx::query_scalar!( + "SELECT is_direct_chat FROM groups WHERE group_id = ?", + group_id + ) + .fetch_optional(&mut **tr) + .await? + .unwrap_or(0) + != 0; + Ok(is_direct) + } + + pub async fn is_member( + tr: &mut Transaction<'_, Sqlite>, + group_id: &str, + contact_id: i64, + ) -> Result { + let is_member = sqlx::query_scalar!( + r#" + SELECT EXISTS( + SELECT 1 + FROM group_members + WHERE group_id = ? AND contact_id = ? + ) + "#, + group_id, + contact_id, + ) + .fetch_one(&mut **tr) + .await? + != 0; + Ok(is_member) + } + pub async fn increase_last_message_exchange_to_now( tr: &mut Transaction<'_, Sqlite>, group_id: &str, @@ -368,7 +420,10 @@ impl InsertGroupHistory { pub struct GetUnjoinedGroups {} impl GetUnjoinedGroups { - pub async fn fetch_all(self, pool: &sqlx::Pool) -> Result> { + pub async fn fetch_all( + self, + pool: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, + ) -> Result> { let ids = sqlx::query_scalar!( "SELECT group_id FROM groups WHERE joined_group = 0 AND left_group = 0" ) diff --git a/rust/src/database/app/tables/mediafiles.rs b/rust/src/database/app/tables/mediafiles.rs new file mode 100644 index 00000000..9bfd1a21 --- /dev/null +++ b/rust/src/database/app/tables/mediafiles.rs @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026, Tobias Müller git@tsmr.eu + * + */ + +use crate::error::Result; +use sqlx::{Sqlite, Transaction}; + +pub struct MediaFile; + +impl MediaFile { + /// Mark a media upload as successful when a receiver response proves that + /// the message (and therefore the media) reached another client. + pub async fn handle_response_from_receiver( + transaction: &mut Transaction<'_, Sqlite>, + message_id: &str, + ) -> Result<()> { + let media_id = sqlx::query_scalar!( + "SELECT media_id FROM messages WHERE message_id = ?", + message_id, + ) + .fetch_optional(&mut **transaction) + .await? + .flatten(); + let Some(media_id) = media_id else { + return Ok(()); + }; + + let updated = sqlx::query!( + "UPDATE media_files SET upload_state = 'uploaded' WHERE media_id = ? AND upload_state IS NOT 'uploaded'", + media_id, + ) + .execute(&mut **transaction) + .await? + .rows_affected(); + if updated == 0 { + return Ok(()); + } + + let now = chrono::Utc::now().timestamp(); + sqlx::query!( + r#" + INSERT INTO message_actions(message_id, contact_id, type, action_at) + SELECT messages.message_id, group_members.contact_id, 'ackByServerAt', ? + FROM messages + JOIN group_members ON group_members.group_id = messages.group_id + WHERE messages.media_id = ? + AND (group_members.member_state IS NULL OR group_members.member_state != 'leftGroup') + ON CONFLICT(message_id, contact_id, type) + DO UPDATE SET action_at = excluded.action_at + "#, + now, + media_id, + ) + .execute(&mut **transaction) + .await?; + + sqlx::query!( + "UPDATE messages SET ack_by_server = ? WHERE media_id = ?", + now, + media_id, + ) + .execute(&mut **transaction) + .await?; + + sqlx::query!( + r#" + UPDATE receipts + SET ack_by_server_at = ?, retry_count = 1, last_retry = ?, mark_for_retry = NULL + WHERE EXISTS( + SELECT 1 + FROM messages + JOIN group_members ON group_members.group_id = messages.group_id + WHERE messages.message_id = receipts.message_id + AND messages.media_id = ? + AND group_members.contact_id = receipts.contact_id + AND (group_members.member_state IS NULL OR group_members.member_state != 'leftGroup') + ) + "#, + now, + now, + media_id, + ) + .execute(&mut **transaction) + .await?; + + tracing::info!( + media_id, + "marked media upload as successful after receiver response" + ); + Ok(()) + } +} diff --git a/rust/src/database/app/tables/mod.rs b/rust/src/database/app/tables/mod.rs index ae2c592a..cd0302ba 100644 --- a/rust/src/database/app/tables/mod.rs +++ b/rust/src/database/app/tables/mod.rs @@ -7,6 +7,7 @@ pub mod contact; pub mod group_history; pub mod groups; pub mod key_verification; +pub mod mediafiles; pub mod message; pub mod receipt; @@ -17,5 +18,6 @@ pub use groups::{ InsertGroup, InsertGroupHistory, InsertGroupMember, UpdateGroup, UpdateGroupMemberState, }; pub use key_verification::{KeyVerification, KeyVerificationType, NewKeyVerification}; +pub use mediafiles::MediaFile; pub use message::{Message, MessageType, NewMessage}; pub use receipt::{NewReceipt, Receipt}; diff --git a/rust/src/database/app/tables/receipt.rs b/rust/src/database/app/tables/receipt.rs index 8e7b6441..2d9e16bf 100644 --- a/rust/src/database/app/tables/receipt.rs +++ b/rust/src/database/app/tables/receipt.rs @@ -29,6 +29,49 @@ pub struct NewReceipt<'a> { } impl Receipt { + pub async fn insert_reaction( + transaction: &mut Transaction<'_, Sqlite>, + message_id: &str, + sender_id: i64, + emoji: &str, + ) -> Result<()> { + sqlx::query!( + r#" + INSERT INTO reactions(message_id, emoji, sender_id) + VALUES (?, ?, ?) + ON CONFLICT(message_id, sender_id, emoji) DO NOTHING + "#, + message_id, + emoji, + sender_id, + ) + .execute(&mut **transaction) + .await?; + + Ok(()) + } + + pub async fn delete_reaction( + transaction: &mut Transaction<'_, Sqlite>, + message_id: &str, + sender_id: i64, + emoji: &str, + ) -> Result<()> { + sqlx::query!( + r#" + DELETE FROM reactions + WHERE message_id = ? AND sender_id = ? AND emoji = ? + "#, + message_id, + sender_id, + emoji, + ) + .execute(&mut **transaction) + .await?; + + Ok(()) + } + pub async fn claim_received( transaction: &mut Transaction<'_, Sqlite>, receipt_id: &str, diff --git a/rust/src/error.rs b/rust/src/error.rs index 3a3cca99..0faac60b 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -3,7 +3,6 @@ * */ -use crate::user_discovery::error::UserDiscoveryError; use hex::FromHexError; use scrypt::errors::{InvalidOutputLen, InvalidParams}; use std::string::FromUtf8Error; @@ -47,8 +46,23 @@ pub enum TwonlyError { #[error("main_key could not be loaded from the key_chain")] MissingMainKey, - #[error("{0}")] - UserDiscoveryError(#[from] UserDiscoveryError), + #[error("User discovery store error: `{0}`")] + UserDiscoveryStore(String), + + #[error("The encrypted announcement data contains malicious data: `{0}`")] + MaliciousAnnouncementData(String), + + #[error("no user-discovery shares left")] + NoSharesLeft, + + #[error("User discovery contains no configuration")] + UserDiscoveryNotInitialized, + + #[error("error while calculating Shamir's secret shares: `{0}`")] + ShamirsSecret(String), + + #[error("tried to push an invalid user-discovery version")] + PushedInvalidVersion, #[error("Error in dart callback")] DartError, @@ -80,6 +94,9 @@ pub enum TwonlyError { #[error("{0}")] IoError(#[from] std::io::Error), + #[error("{0}")] + JsonError(#[from] serde_json::Error), + #[error("{0}")] ZipError(#[from] ZipError), @@ -121,14 +138,14 @@ impl From for TwonlyError { } } -impl From for UserDiscoveryError { - fn from(error: TwonlyError) -> Self { - UserDiscoveryError::Store(error.to_string()) - } -} - impl From for TwonlyError { fn from(_: aes_gcm::Error) -> Self { TwonlyError::AesGcm } } + +impl From for TwonlyError { + fn from(error: libsignal_protocol::SignalProtocolError) -> Self { + TwonlyError::Signal(error.to_string()) + } +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 9715cc67..addab330 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -392823520; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -937778436; // Section: executor @@ -272,6 +272,23 @@ fn wire__crate__bridge__groups__fetch_missing_group_public_keys_impl( }, ) } +fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_change_exclusion_for_contact", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { + let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_callback_id = ::sse_decode(&mut deserializer); +let api_contact_id = ::sse_decode(&mut deserializer); +let api_exclude = ::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::change_exclusion_for_contact(api_callback_id, api_contact_id, api_exclude).await?; Ok(output_ok) + })().await) + } }) +} fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -287,77 +304,6 @@ fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_curr })().await) } }) } -fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_new_messages_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_get_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { - let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; - let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_callback_id = ::sse_decode(&mut deserializer); -let api_contact_id = ::sse_decode(&mut deserializer); -let api_received_version = >::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { - let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::get_new_messages(api_callback_id, api_contact_id, &api_received_version).await?; Ok(output_ok) - })().await) - } }) -} -fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_new_messages_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_handle_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { - let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; - let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_callback_id = ::sse_decode(&mut deserializer); -let api_contact_id = ::sse_decode(&mut deserializer); -let api_public_key_verified_timestamp = >::sse_decode(&mut deserializer); -let api_messages = >>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { - let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::handle_new_messages(api_callback_id, api_contact_id, api_public_key_verified_timestamp, api_messages).await?; Ok(output_ok) - })().await) - } }) -} -fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_initialize_or_update_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_initialize_or_update", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { - let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; - let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_callback_id = ::sse_decode(&mut deserializer); -let api_threshold = ::sse_decode(&mut deserializer); -let api_user_id = ::sse_decode(&mut deserializer); -let api_public_key = >::sse_decode(&mut deserializer); -let api_share_promotion = ::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { - let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::initialize_or_update(api_callback_id, api_threshold, api_user_id, api_public_key, api_share_promotion).await?; Ok(output_ok) - })().await) - } }) -} -fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_should_request_new_messages_impl( - port_: flutter_rust_bridge::for_generated::MessagePort, - ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, - rust_vec_len_: i32, - data_len_: i32, -) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "flutter_user_discovery_should_request_new_messages", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || { - let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) }; - let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_callback_id = ::sse_decode(&mut deserializer); -let api_contact_id = ::sse_decode(&mut deserializer); -let api_version = >::sse_decode(&mut deserializer);deserializer.end(); move |context| async move { - transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { - let output_ok = crate::bridge::wrapper::user_discovery::FlutterUserDiscovery::should_request_new_messages(api_callback_id, api_contact_id, &api_version).await?; Ok(output_ok) - })().await) - } }) -} fn wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -390,17 +336,13 @@ let api_legacy_signal_decrypt = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_i let api_legacy_signal_encrypt = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(::sse_decode(&mut deserializer)); let api_legacy_signal_generate_prekeys = decode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(::sse_decode(&mut deserializer)); let api_api_resync_signal_session = decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); -let api_api_push_key_requested = decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); -let api_api_group_membership_error = decode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); let api_api_media_action = decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); let api_api_verification_proof = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); -let api_api_create_push_data = decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException(::sse_decode(&mut deserializer)); let api_api_create_push_avatars = decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); -let api_api_recovery_changed = decode_DartFn_Inputs__Output_unit_AnyhowException(::sse_decode(&mut deserializer)); let api_api_media_received = decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); -let api_api_group_state_refresh = decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(::sse_decode(&mut deserializer));deserializer.end(); move |context| { +let api_api_user_config_changed = decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(::sse_decode(&mut deserializer));deserializer.end(); move |context| { transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_,()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks(api_callback_id, api_logging_get_stream_sink, api_legacy_signal_decrypt, api_legacy_signal_encrypt, api_legacy_signal_generate_prekeys, api_api_resync_signal_session, api_api_push_key_requested, api_api_group_membership_error, api_api_media_action, api_api_verification_proof, api_api_create_push_data, api_api_create_push_avatars, api_api_recovery_changed, api_api_media_received, api_api_group_state_refresh); })?; Ok(output_ok) + let output_ok = Result::<_,()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks(api_callback_id, api_logging_get_stream_sink, api_legacy_signal_decrypt, api_legacy_signal_encrypt, api_legacy_signal_generate_prekeys, api_api_resync_signal_session, api_api_media_action, api_api_verification_proof, api_api_create_push_avatars, api_api_media_received, api_api_user_config_changed); })?; Ok(output_ok) })()) } }) } @@ -1110,6 +1052,79 @@ fn wire__crate__bridge__api__rust_api_download_done_impl( }, ) } +fn wire__crate__bridge__api__rust_api_download_media_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "rust_api_download_media", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_media_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::api::RustApi::download_media(api_media_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__api__rust_api_download_pending_media_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "rust_api_download_pending_media", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::api::RustApi::download_pending_media().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__api__rust_api_events_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1725,13 +1740,11 @@ fn wire__crate__bridge__api__rust_api_load_plan_balance_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_use_cache = ::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( (move || async move { - let output_ok = - crate::bridge::api::RustApi::load_plan_balance(api_use_cache).await?; + let output_ok = crate::bridge::api::RustApi::load_plan_balance().await?; Ok(output_ok) })() .await, @@ -1781,6 +1794,43 @@ fn wire__crate__bridge__api__rust_api_notify_messages_opened_impl( }, ) } +fn wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "rust_api_perform_passwordless_recovery_heartbeat", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::api::RustApi::perform_passwordless_recovery_heartbeat() + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__api__rust_api_prepare_queued_message_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -2139,6 +2189,44 @@ fn wire__crate__bridge__api__rust_api_request_contact_by_username_impl( }, ) } +fn wire__crate__bridge__api__rust_api_request_media_reupload_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "rust_api_request_media_reupload", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_media_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::api::RustApi::request_media_reupload(api_media_id) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__api__rust_api_request_memories_upload_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -3765,6 +3853,234 @@ fn wire__crate__bridge__groups__update_group_name_impl( }, ) } +fn wire__crate__bridge__user_config__user_config_api_clone_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "user_config_api_clone", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_config = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::bridge::user_config::UserConfigApi::clone(api_config), + )?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__bridge__user_config__user_config_api_create_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "user_config_api_create", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_user_id = ::sse_decode(&mut deserializer); + let api_username = ::sse_decode(&mut deserializer); + let api_display_name = ::sse_decode(&mut deserializer); + let api_current_setup_page = >::sse_decode(&mut deserializer); + let api_app_version = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::bridge::user_config::UserConfigApi::create( + api_user_id, + api_username, + api_display_name, + api_current_setup_page, + api_app_version, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__user_config__user_config_api_import_json_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "user_config_api_import_json", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_json = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::user_config::UserConfigApi::import_json(api_json) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__user_config__user_config_api_load_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "user_config_api_load", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::bridge::user_config::UserConfigApi::load().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__user_config__user_config_api_save_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "user_config_api_save", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_config = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::user_config::UserConfigApi::save(api_config).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__user_config__user_config_api_update_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "user_config_api_update", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_base = ::sse_decode(&mut deserializer); + let api_config = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::user_config::UserConfigApi::update(api_base, api_config) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} // Section: related_funcs @@ -3814,42 +4130,6 @@ fn decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( )) } } -fn decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException( - dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn(String, bool) -> flutter_rust_bridge::DartFnFuture<()> { - use flutter_rust_bridge::IntoDart; - - async fn body(dart_opaque: flutter_rust_bridge::DartOpaque, arg0: String, arg1: bool) -> () { - let args = vec![ - arg0.into_into_dart().into_dart(), - arg1.into_into_dart().into_dart(), - ]; - let message = FLUTTER_RUST_BRIDGE_HANDLER - .dart_fn_invoke(dart_opaque, args) - .await; - - let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let action = deserializer.cursor.read_u8().unwrap(); - let ans = match action { - 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), - 1 => std::result::Result::Err( - ::sse_decode(&mut deserializer), - ), - _ => unreachable!(), - }; - deserializer.end(); - let ans = ans.expect("Dart throws exception but Rust side assume it is not failable"); - ans - } - - move |arg0: String, arg1: bool| { - flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( - dart_opaque.clone(), - arg0, - arg1, - )) - } -} fn decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, ) -> impl Fn(String, i64) -> flutter_rust_bridge::DartFnFuture<()> { @@ -3956,35 +4236,6 @@ fn decode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException( flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body(dart_opaque.clone())) } } -fn decode_DartFn_Inputs__Output_unit_AnyhowException( - dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn() -> flutter_rust_bridge::DartFnFuture<()> { - use flutter_rust_bridge::IntoDart; - - async fn body(dart_opaque: flutter_rust_bridge::DartOpaque) -> () { - let args = vec![]; - let message = FLUTTER_RUST_BRIDGE_HANDLER - .dart_fn_invoke(dart_opaque, args) - .await; - - let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let action = deserializer.cursor.read_u8().unwrap(); - let ans = match action { - 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), - 1 => std::result::Result::Err( - ::sse_decode(&mut deserializer), - ), - _ => unreachable!(), - }; - deserializer.end(); - let ans = ans.expect("Dart throws exception but Rust side assume it is not failable"); - ans - } - - move || { - flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body(dart_opaque.clone())) - } -} fn decode_DartFn_Inputs_i_64_Output_unit_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, ) -> impl Fn(i64) -> flutter_rust_bridge::DartFnFuture<()> { @@ -4017,49 +4268,6 @@ fn decode_DartFn_Inputs_i_64_Output_unit_AnyhowException( )) } } -fn decode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException( - dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn(i64, String, String) -> flutter_rust_bridge::DartFnFuture<()> { - use flutter_rust_bridge::IntoDart; - - async fn body( - dart_opaque: flutter_rust_bridge::DartOpaque, - arg0: i64, - arg1: String, - arg2: String, - ) -> () { - let args = vec![ - arg0.into_into_dart().into_dart(), - arg1.into_into_dart().into_dart(), - arg2.into_into_dart().into_dart(), - ]; - let message = FLUTTER_RUST_BRIDGE_HANDLER - .dart_fn_invoke(dart_opaque, args) - .await; - - let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let action = deserializer.cursor.read_u8().unwrap(); - let ans = match action { - 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), - 1 => std::result::Result::Err( - ::sse_decode(&mut deserializer), - ), - _ => unreachable!(), - }; - deserializer.end(); - let ans = ans.expect("Dart throws exception but Rust side assume it is not failable"); - ans - } - - move |arg0: i64, arg1: String, arg2: String| { - flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( - dart_opaque.clone(), - arg0, - arg1, - arg2, - )) - } -} fn decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, ) -> impl Fn( @@ -4194,25 +4402,16 @@ fn decode_DartFn_Inputs_i_64_list_prim_u_8_strict_i_32_Output_legacy_signal_decr )) } } -fn decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( +fn decode_DartFn_Inputs_user_config_Output_unit_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn(i64, Option, Vec, i32) -> flutter_rust_bridge::DartFnFuture>> -{ +) -> impl Fn(crate::user_config::UserConfig) -> flutter_rust_bridge::DartFnFuture<()> { use flutter_rust_bridge::IntoDart; async fn body( dart_opaque: flutter_rust_bridge::DartOpaque, - arg0: i64, - arg1: Option, - arg2: Vec, - arg3: i32, - ) -> Option> { - let args = vec![ - arg0.into_into_dart().into_dart(), - arg1.into_into_dart().into_dart(), - arg2.into_into_dart().into_dart(), - arg3.into_into_dart().into_dart(), - ]; + arg0: crate::user_config::UserConfig, + ) -> () { + let args = vec![arg0.into_into_dart().into_dart()]; let message = FLUTTER_RUST_BRIDGE_HANDLER .dart_fn_invoke(dart_opaque, args) .await; @@ -4220,7 +4419,7 @@ fn decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_lis let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let action = deserializer.cursor.read_u8().unwrap(); let ans = match action { - 0 => std::result::Result::Ok(>>::sse_decode(&mut deserializer)), + 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), 1 => std::result::Result::Err( ::sse_decode(&mut deserializer), ), @@ -4231,13 +4430,10 @@ fn decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_lis ans } - move |arg0: i64, arg1: Option, arg2: Vec, arg3: i32| { + move |arg0: crate::user_config::UserConfig| { flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( dart_opaque.clone(), arg0, - arg1, - arg2, - arg3, )) } } @@ -4252,6 +4448,19 @@ impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { } } +impl SseDecode for chrono::DateTime { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return chrono::DateTime::::from_naive_utc_and_offset( + chrono::DateTime::from_timestamp_micros(inner) + .expect("invalid or out-of-range datetime") + .naive_utc(), + chrono::Utc, + ); + } +} + impl SseDecode for flutter_rust_bridge::DartOpaque { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4260,6 +4469,14 @@ impl SseDecode for flutter_rust_bridge::DartOpaque { } } +impl SseDecode for std::collections::HashMap> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = )>>::sse_decode(deserializer); + return inner.into_iter().collect(); + } +} + impl SseDecode for std::collections::HashMap> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4458,6 +4675,20 @@ impl SseDecode for isize { } } +impl SseDecode for crate::user_config::LastBackupUploadState { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::user_config::LastBackupUploadState::None, + 1 => crate::user_config::LastBackupUploadState::Pending, + 2 => crate::user_config::LastBackupUploadState::Failed, + 3 => crate::user_config::LastBackupUploadState::Success, + _ => unreachable!("Invalid variant for LastBackupUploadState: {}", inner), + }; + } +} + impl SseDecode for crate::bridge::wrapper::app_database::LegacyMigrationReport { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4639,6 +4870,18 @@ impl SseDecode for Vec<(i64, Vec)> { } } +impl SseDecode for Vec<(String, Vec)> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = Vec::with_capacity(len_ as usize); + for idx_ in 0..len_ { + ans_.push(<(String, Vec)>::sse_decode(deserializer)); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4663,6 +4906,19 @@ impl SseDecode for Vec { } } +impl SseDecode for Option>> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some( + >>::sse_decode(deserializer), + ); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4674,6 +4930,17 @@ impl SseDecode for Option { } } +impl SseDecode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(>::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4733,6 +5000,19 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some( + ::sse_decode(deserializer), + ); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4746,6 +5026,19 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode( + deserializer, + )); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4757,6 +5050,28 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(>::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4768,6 +5083,30 @@ impl SseDecode for Option> { } } +impl SseDecode for crate::user_config::PasswordlessRecoveryConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_email = >::sse_decode(deserializer); + let mut var_threshold = ::sse_decode(deserializer); + let mut var_serverKeyProtection = >>::sse_decode(deserializer); + let mut var_pinUnlockToken = >>::sse_decode(deserializer); + let mut var_lastServerHeartbeat = + >>::sse_decode(deserializer); + let mut var_lastContactHeartbeat = + >>::sse_decode(deserializer); + let mut var_encryptedServerKey = >>::sse_decode(deserializer); + return crate::user_config::PasswordlessRecoveryConfig { + email: var_email, + threshold: var_threshold, + server_key_protection: var_serverKeyProtection, + pin_unlock_token: var_pinUnlockToken, + last_server_heartbeat: var_lastServerHeartbeat, + last_contact_heartbeat: var_lastContactHeartbeat, + encrypted_server_key: var_encryptedServerKey, + }; + } +} + impl SseDecode for crate::api::server::prekeys::PqcPreKeyInput { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4816,6 +5155,15 @@ impl SseDecode for (Vec, i64) { } } +impl SseDecode for (String, Vec) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = >::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + impl SseDecode for (String, String) { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4874,62 +5222,15 @@ impl SseDecode for crate::bridge::wrapper::RustUtils { } } -impl SseDecode for crate::bridge::api::ServerResultEmpty { +impl SseDecode for crate::user_config::SetupProfile { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut tag_ = ::sse_decode(deserializer); - match tag_ { - 0 => { - return crate::bridge::api::ServerResultEmpty::Ok; - } - 1 => { - let mut var_field0 = ::sse_decode(deserializer); - return crate::bridge::api::ServerResultEmpty::ErrorCode(var_field0); - } - _ => { - unimplemented!(""); - } - } - } -} - -impl SseDecode for crate::bridge::api::ServerResultI64 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut tag_ = ::sse_decode(deserializer); - match tag_ { - 0 => { - let mut var_field0 = ::sse_decode(deserializer); - return crate::bridge::api::ServerResultI64::Ok(var_field0); - } - 1 => { - let mut var_field0 = ::sse_decode(deserializer); - return crate::bridge::api::ServerResultI64::ErrorCode(var_field0); - } - _ => { - unimplemented!(""); - } - } - } -} - -impl SseDecode for crate::bridge::api::ServerResultVecU8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut tag_ = ::sse_decode(deserializer); - match tag_ { - 0 => { - let mut var_field0 = >::sse_decode(deserializer); - return crate::bridge::api::ServerResultVecU8::Ok(var_field0); - } - 1 => { - let mut var_field0 = ::sse_decode(deserializer); - return crate::bridge::api::ServerResultVecU8::ErrorCode(var_field0); - } - _ => { - unimplemented!(""); - } - } + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::user_config::SetupProfile::Standard, + 1 => crate::user_config::SetupProfile::Customized, + _ => unreachable!("Invalid variant for SetupProfile: {}", inner), + }; } } @@ -4983,6 +5284,39 @@ impl SseDecode for crate::database::app::SqlValue { } } +impl SseDecode for crate::user_config::ThemeMode { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::user_config::ThemeMode::System, + 1 => crate::user_config::ThemeMode::Light, + 2 => crate::user_config::ThemeMode::Dark, + _ => unreachable!("Invalid variant for ThemeMode: {}", inner), + }; + } +} + +impl SseDecode for crate::user_config::TwonlySafeBackup { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_lastBackupSize = ::sse_decode(deserializer); + let mut var_backupUploadState = + ::sse_decode(deserializer); + let mut var_lastBackupDone = + >>::sse_decode(deserializer); + let mut var_backupId = >::sse_decode(deserializer); + let mut var_encryptionKey = >::sse_decode(deserializer); + return crate::user_config::TwonlySafeBackup { + last_backup_size: var_lastBackupSize, + backup_upload_state: var_backupUploadState, + last_backup_done: var_lastBackupDone, + backup_id: var_backupId, + encryption_key: var_encryptionKey, + }; + } +} + impl SseDecode for u32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -5017,6 +5351,144 @@ impl SseDecode for () { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} } +impl SseDecode for crate::user_config::UserConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_userId = ::sse_decode(deserializer); + let mut var_username = ::sse_decode(deserializer); + let mut var_displayName = ::sse_decode(deserializer); + let mut var_avatarSvg = >::sse_decode(deserializer); + let mut var_appVersion = ::sse_decode(deserializer); + let mut var_avatarCounter = ::sse_decode(deserializer); + let mut var_videoStabilizationEnabled = ::sse_decode(deserializer); + let mut var_isDeveloper = ::sse_decode(deserializer); + let mut var_deviceId = ::sse_decode(deserializer); + let mut var_setupProfile = ::sse_decode(deserializer); + let mut var_subscriptionPlan = ::sse_decode(deserializer); + let mut var_subscriptionPlanIdStore = >::sse_decode(deserializer); + let mut var_lastImageSend = + >>::sse_decode(deserializer); + let mut var_todaysImageCounter = >::sse_decode(deserializer); + let mut var_lastPlanBallance = >::sse_decode(deserializer); + let mut var_additionalUserInvites = >::sse_decode(deserializer); + let mut var_themeMode = ::sse_decode(deserializer); + let mut var_primaryColorValue = >::sse_decode(deserializer); + let mut var_defaultShowTime = >::sse_decode(deserializer); + let mut var_requestedAudioPermission = ::sse_decode(deserializer); + let mut var_enableDatabaseLogging = ::sse_decode(deserializer); + let mut var_automaticallyMarkEqualMediaFilesAsOpened = ::sse_decode(deserializer); + let mut var_showNewsShortcut = ::sse_decode(deserializer); + let mut var_showShowImagePreviewWhenSending = ::sse_decode(deserializer); + let mut var_startWithCameraOpen = ::sse_decode(deserializer); + let mut var_preSelectedEmojies = >>::sse_decode(deserializer); + let mut var_autoDownloadOptions = + >>>::sse_decode(deserializer); + let mut var_storeMediaFilesInGallery = ::sse_decode(deserializer); + let mut var_autoStoreAllSendUnlimitedMediaFiles = ::sse_decode(deserializer); + let mut var_typingIndicators = ::sse_decode(deserializer); + let mut var_showRestoreFlame = ::sse_decode(deserializer); + let mut var_myBestFriendGroupId = >::sse_decode(deserializer); + let mut var_signalLastSignedPreKeyUpdated = + >>::sse_decode(deserializer); + let mut var_signalLastPqcPreKeysUploaded = + >>::sse_decode(deserializer); + let mut var_allowErrorTrackingViaSentry = ::sse_decode(deserializer); + let mut var_screenLockEnabled = ::sse_decode(deserializer); + let mut var_isCloudBackupEnabled = ::sse_decode(deserializer); + let mut var_isUserDiscoveryEnabled = ::sse_decode(deserializer); + let mut var_requiredSendImages = ::sse_decode(deserializer); + let mut var_userDiscoveryThreshold = ::sse_decode(deserializer); + let mut var_userDiscoveryRequiresManualApproval = ::sse_decode(deserializer); + let mut var_userDiscoverySharePromotion = ::sse_decode(deserializer); + let mut var_userDiscoveryInitializationError = ::sse_decode(deserializer); + let mut var_askForFriendPromotions = ::sse_decode(deserializer); + let mut var_currentPreKeyIndexStart = ::sse_decode(deserializer); + let mut var_currentSignedPreKeyIndexStart = ::sse_decode(deserializer); + let mut var_lastChangeLogHash = >>::sse_decode(deserializer); + let mut var_hideChangeLog = ::sse_decode(deserializer); + let mut var_hideMemoriesBackupPromo = ::sse_decode(deserializer); + let mut var_updateFcmToken = ::sse_decode(deserializer); + let mut var_canUseLoginTokenForAuth = ::sse_decode(deserializer); + let mut var_twonlySafeBackup = + >::sse_decode(deserializer); + let mut var_isBackupEnabled = ::sse_decode(deserializer); + let mut var_passwordLessRecovery = + >::sse_decode(deserializer); + let mut var_fcmToken = >::sse_decode(deserializer); + let mut var_currentSetupPage = >::sse_decode(deserializer); + let mut var_skipSetupPages = ::sse_decode(deserializer); + let mut var_hasZoomed = ::sse_decode(deserializer); + return crate::user_config::UserConfig { + user_id: var_userId, + username: var_username, + display_name: var_displayName, + avatar_svg: var_avatarSvg, + app_version: var_appVersion, + avatar_counter: var_avatarCounter, + video_stabilization_enabled: var_videoStabilizationEnabled, + is_developer: var_isDeveloper, + device_id: var_deviceId, + setup_profile: var_setupProfile, + subscription_plan: var_subscriptionPlan, + subscription_plan_id_store: var_subscriptionPlanIdStore, + last_image_send: var_lastImageSend, + todays_image_counter: var_todaysImageCounter, + last_plan_ballance: var_lastPlanBallance, + additional_user_invites: var_additionalUserInvites, + theme_mode: var_themeMode, + primary_color_value: var_primaryColorValue, + default_show_time: var_defaultShowTime, + requested_audio_permission: var_requestedAudioPermission, + enable_database_logging: var_enableDatabaseLogging, + automatically_mark_equal_media_files_as_opened: + var_automaticallyMarkEqualMediaFilesAsOpened, + show_news_shortcut: var_showNewsShortcut, + show_show_image_preview_when_sending: var_showShowImagePreviewWhenSending, + start_with_camera_open: var_startWithCameraOpen, + pre_selected_emojies: var_preSelectedEmojies, + auto_download_options: var_autoDownloadOptions, + store_media_files_in_gallery: var_storeMediaFilesInGallery, + auto_store_all_send_unlimited_media_files: var_autoStoreAllSendUnlimitedMediaFiles, + typing_indicators: var_typingIndicators, + show_restore_flame: var_showRestoreFlame, + my_best_friend_group_id: var_myBestFriendGroupId, + signal_last_signed_pre_key_updated: var_signalLastSignedPreKeyUpdated, + signal_last_pqc_pre_keys_uploaded: var_signalLastPqcPreKeysUploaded, + allow_error_tracking_via_sentry: var_allowErrorTrackingViaSentry, + screen_lock_enabled: var_screenLockEnabled, + is_cloud_backup_enabled: var_isCloudBackupEnabled, + is_user_discovery_enabled: var_isUserDiscoveryEnabled, + required_send_images: var_requiredSendImages, + user_discovery_threshold: var_userDiscoveryThreshold, + user_discovery_requires_manual_approval: var_userDiscoveryRequiresManualApproval, + user_discovery_share_promotion: var_userDiscoverySharePromotion, + user_discovery_initialization_error: var_userDiscoveryInitializationError, + ask_for_friend_promotions: var_askForFriendPromotions, + current_pre_key_index_start: var_currentPreKeyIndexStart, + current_signed_pre_key_index_start: var_currentSignedPreKeyIndexStart, + last_change_log_hash: var_lastChangeLogHash, + hide_change_log: var_hideChangeLog, + hide_memories_backup_promo: var_hideMemoriesBackupPromo, + update_fcm_token: var_updateFcmToken, + can_use_login_token_for_auth: var_canUseLoginTokenForAuth, + twonly_safe_backup: var_twonlySafeBackup, + is_backup_enabled: var_isBackupEnabled, + password_less_recovery: var_passwordLessRecovery, + fcm_token: var_fcmToken, + current_setup_page: var_currentSetupPage, + skip_setup_pages: var_skipSetupPages, + has_zoomed: var_hasZoomed, + }; + } +} + +impl SseDecode for crate::bridge::user_config::UserConfigApi { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + return crate::bridge::user_config::UserConfigApi {}; + } +} + impl SseDecode for usize { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -5039,48 +5511,48 @@ fn pde_ffi_dispatcher_primary_impl( 4 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len), 5 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len), 6 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len), -7 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len), -8 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_new_messages_impl(port, ptr, rust_vec_len, data_len), -9 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_handle_new_messages_impl(port, ptr, rust_vec_len, data_len), -10 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_initialize_or_update_impl(port, ptr, rust_vec_len, data_len), -11 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_should_request_new_messages_impl(port, ptr, rust_vec_len, data_len), -12 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), -13 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), -14 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), -15 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len), -16 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len), -17 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len), -18 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len), -19 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len), -20 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len), -22 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len), -23 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len), -24 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), -25 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len), -26 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), -27 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), -28 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), -29 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), -30 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), -31 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), -32 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), -33 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), -34 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), -35 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), -36 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), -37 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), -38 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), -39 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -40 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), -41 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), -42 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), -43 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), -44 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), -45 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), -46 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), -47 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), -48 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), -49 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), +7 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len), +8 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len), +9 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), +10 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), +11 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), +12 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len), +13 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len), +14 => wire__crate__bridge__groups__manage_admin_state_impl(port, ptr, rust_vec_len, data_len), +15 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len), +16 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len), +17 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len), +19 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len), +20 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len), +21 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), +22 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len), +23 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), +24 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), +25 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), +26 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), +27 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), +28 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), +29 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), +30 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), +31 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), +32 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), +33 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), +34 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), +35 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), +36 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), +37 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), +38 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +39 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), +40 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), +41 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), +42 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), +43 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), +44 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), +45 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), +46 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), +47 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), +48 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), +49 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), 50 => wire__crate__bridge__api__rust_api_prepare_queued_message_impl(port, ptr, rust_vec_len, data_len), 51 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), 52 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), @@ -5090,58 +5562,64 @@ fn pde_ffi_dispatcher_primary_impl( 56 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), 57 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), 58 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), -59 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), -60 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), -61 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), -62 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), -63 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), -64 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), -65 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), -66 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), -67 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), -68 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), -69 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), -70 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), -71 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), -72 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), -73 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), -74 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), -75 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), -76 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), -77 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), -78 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), -79 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), -80 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), -81 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), -82 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), -83 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -84 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), -85 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -86 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), -87 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -88 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -89 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -90 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len), -91 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), -92 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), -93 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), -94 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), -95 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len), -96 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len), -97 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), -98 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len), -99 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), -100 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), -101 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len), -102 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), -103 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), -104 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), -105 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), -106 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), -107 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), -108 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), -109 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), -110 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), +59 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), +60 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), +61 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), +62 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), +63 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), +64 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), +65 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), +66 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), +67 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), +68 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), +69 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), +70 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), +71 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), +72 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), +73 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), +74 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), +75 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), +76 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), +77 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), +78 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), +79 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), +80 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), +81 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), +82 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), +83 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), +84 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +85 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), +86 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +87 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), +88 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +89 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +90 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +91 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len), +92 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), +93 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +94 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), +95 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +96 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +97 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len), +98 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), +99 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +100 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), +101 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +102 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +103 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), +104 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), +105 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), +106 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), +107 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), +108 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), +109 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), +110 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), +111 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), +113 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), +114 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), +115 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), +116 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), +117 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -5154,7 +5632,12 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { - 21 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), + 18 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), + 112 => wire__crate__bridge__user_config__user_config_api_clone_impl( + ptr, + rust_vec_len, + data_len, + ), _ => unreachable!(), } } @@ -5344,6 +5827,29 @@ impl flutter_rust_bridge::IntoIntoDart for crate::bri } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::user_config::LastBackupUploadState { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::None => 0.into_dart(), + Self::Pending => 1.into_dart(), + Self::Failed => 2.into_dart(), + Self::Success => 3.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::user_config::LastBackupUploadState +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::user_config::LastBackupUploadState +{ + fn into_into_dart(self) -> crate::user_config::LastBackupUploadState { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::bridge::wrapper::app_database::LegacyMigrationReport { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -5453,6 +5959,32 @@ impl } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::user_config::PasswordlessRecoveryConfig { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.email.into_into_dart().into_dart(), + self.threshold.into_into_dart().into_dart(), + self.server_key_protection.into_into_dart().into_dart(), + self.pin_unlock_token.into_into_dart().into_dart(), + self.last_server_heartbeat.into_into_dart().into_dart(), + self.last_contact_heartbeat.into_into_dart().into_dart(), + self.encrypted_server_key.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::user_config::PasswordlessRecoveryConfig +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::user_config::PasswordlessRecoveryConfig +{ + fn into_into_dart(self) -> crate::user_config::PasswordlessRecoveryConfig { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::server::prekeys::PqcPreKeyInput { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -5614,81 +6146,23 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::bridge::api::ServerResultEmpty { +impl flutter_rust_bridge::IntoDart for crate::user_config::SetupProfile { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { - crate::bridge::api::ServerResultEmpty::Ok => [0.into_dart()].into_dart(), - crate::bridge::api::ServerResultEmpty::ErrorCode(field0) => { - [1.into_dart(), field0.into_into_dart().into_dart()].into_dart() - } - _ => { - unimplemented!(""); - } + Self::Standard => 0.into_dart(), + Self::Customized => 1.into_dart(), + _ => unreachable!(), } } } impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::bridge::api::ServerResultEmpty + for crate::user_config::SetupProfile { } -impl flutter_rust_bridge::IntoIntoDart - for crate::bridge::api::ServerResultEmpty +impl flutter_rust_bridge::IntoIntoDart + for crate::user_config::SetupProfile { - fn into_into_dart(self) -> crate::bridge::api::ServerResultEmpty { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::bridge::api::ServerResultI64 { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - match self { - crate::bridge::api::ServerResultI64::Ok(field0) => { - [0.into_dart(), field0.into_into_dart().into_dart()].into_dart() - } - crate::bridge::api::ServerResultI64::ErrorCode(field0) => { - [1.into_dart(), field0.into_into_dart().into_dart()].into_dart() - } - _ => { - unimplemented!(""); - } - } - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::bridge::api::ServerResultI64 -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::bridge::api::ServerResultI64 -{ - fn into_into_dart(self) -> crate::bridge::api::ServerResultI64 { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs -impl flutter_rust_bridge::IntoDart for crate::bridge::api::ServerResultVecU8 { - fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { - match self { - crate::bridge::api::ServerResultVecU8::Ok(field0) => { - [0.into_dart(), field0.into_into_dart().into_dart()].into_dart() - } - crate::bridge::api::ServerResultVecU8::ErrorCode(field0) => { - [1.into_dart(), field0.into_into_dart().into_dart()].into_dart() - } - _ => { - unimplemented!(""); - } - } - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::bridge::api::ServerResultVecU8 -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::bridge::api::ServerResultVecU8 -{ - fn into_into_dart(self) -> crate::bridge::api::ServerResultVecU8 { + fn into_into_dart(self) -> crate::user_config::SetupProfile { self } } @@ -5769,6 +6243,171 @@ impl flutter_rust_bridge::IntoIntoDart self } } +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::user_config::ThemeMode { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::System => 0.into_dart(), + Self::Light => 1.into_dart(), + Self::Dark => 2.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::user_config::ThemeMode {} +impl flutter_rust_bridge::IntoIntoDart + for crate::user_config::ThemeMode +{ + fn into_into_dart(self) -> crate::user_config::ThemeMode { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::user_config::TwonlySafeBackup { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.last_backup_size.into_into_dart().into_dart(), + self.backup_upload_state.into_into_dart().into_dart(), + self.last_backup_done.into_into_dart().into_dart(), + self.backup_id.into_into_dart().into_dart(), + self.encryption_key.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::user_config::TwonlySafeBackup +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::user_config::TwonlySafeBackup +{ + fn into_into_dart(self) -> crate::user_config::TwonlySafeBackup { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.user_id.into_into_dart().into_dart(), + self.username.into_into_dart().into_dart(), + self.display_name.into_into_dart().into_dart(), + self.avatar_svg.into_into_dart().into_dart(), + self.app_version.into_into_dart().into_dart(), + self.avatar_counter.into_into_dart().into_dart(), + self.video_stabilization_enabled + .into_into_dart() + .into_dart(), + self.is_developer.into_into_dart().into_dart(), + self.device_id.into_into_dart().into_dart(), + self.setup_profile.into_into_dart().into_dart(), + self.subscription_plan.into_into_dart().into_dart(), + self.subscription_plan_id_store.into_into_dart().into_dart(), + self.last_image_send.into_into_dart().into_dart(), + self.todays_image_counter.into_into_dart().into_dart(), + self.last_plan_ballance.into_into_dart().into_dart(), + self.additional_user_invites.into_into_dart().into_dart(), + self.theme_mode.into_into_dart().into_dart(), + self.primary_color_value.into_into_dart().into_dart(), + self.default_show_time.into_into_dart().into_dart(), + self.requested_audio_permission.into_into_dart().into_dart(), + self.enable_database_logging.into_into_dart().into_dart(), + self.automatically_mark_equal_media_files_as_opened + .into_into_dart() + .into_dart(), + self.show_news_shortcut.into_into_dart().into_dart(), + self.show_show_image_preview_when_sending + .into_into_dart() + .into_dart(), + self.start_with_camera_open.into_into_dart().into_dart(), + self.pre_selected_emojies.into_into_dart().into_dart(), + self.auto_download_options.into_into_dart().into_dart(), + self.store_media_files_in_gallery + .into_into_dart() + .into_dart(), + self.auto_store_all_send_unlimited_media_files + .into_into_dart() + .into_dart(), + self.typing_indicators.into_into_dart().into_dart(), + self.show_restore_flame.into_into_dart().into_dart(), + self.my_best_friend_group_id.into_into_dart().into_dart(), + self.signal_last_signed_pre_key_updated + .into_into_dart() + .into_dart(), + self.signal_last_pqc_pre_keys_uploaded + .into_into_dart() + .into_dart(), + self.allow_error_tracking_via_sentry + .into_into_dart() + .into_dart(), + self.screen_lock_enabled.into_into_dart().into_dart(), + self.is_cloud_backup_enabled.into_into_dart().into_dart(), + self.is_user_discovery_enabled.into_into_dart().into_dart(), + self.required_send_images.into_into_dart().into_dart(), + self.user_discovery_threshold.into_into_dart().into_dart(), + self.user_discovery_requires_manual_approval + .into_into_dart() + .into_dart(), + self.user_discovery_share_promotion + .into_into_dart() + .into_dart(), + self.user_discovery_initialization_error + .into_into_dart() + .into_dart(), + self.ask_for_friend_promotions.into_into_dart().into_dart(), + self.current_pre_key_index_start + .into_into_dart() + .into_dart(), + self.current_signed_pre_key_index_start + .into_into_dart() + .into_dart(), + self.last_change_log_hash.into_into_dart().into_dart(), + self.hide_change_log.into_into_dart().into_dart(), + self.hide_memories_backup_promo.into_into_dart().into_dart(), + self.update_fcm_token.into_into_dart().into_dart(), + self.can_use_login_token_for_auth + .into_into_dart() + .into_dart(), + self.twonly_safe_backup.into_into_dart().into_dart(), + self.is_backup_enabled.into_into_dart().into_dart(), + self.password_less_recovery.into_into_dart().into_dart(), + self.fcm_token.into_into_dart().into_dart(), + self.current_setup_page.into_into_dart().into_dart(), + self.skip_setup_pages.into_into_dart().into_dart(), + self.has_zoomed.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::user_config::UserConfig +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::user_config::UserConfig +{ + fn into_into_dart(self) -> crate::user_config::UserConfig { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::bridge::user_config::UserConfigApi { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + Vec::::new().into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::bridge::user_config::UserConfigApi +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::bridge::user_config::UserConfigApi +{ + fn into_into_dart(self) -> crate::bridge::user_config::UserConfigApi { + self + } +} impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5777,6 +6416,13 @@ impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { } } +impl SseEncode for chrono::DateTime { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.timestamp_micros(), serializer); + } +} + impl SseEncode for flutter_rust_bridge::DartOpaque { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -5784,6 +6430,13 @@ impl SseEncode for flutter_rust_bridge::DartOpaque { } } +impl SseEncode for std::collections::HashMap> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + )>>::sse_encode(self.into_iter().collect(), serializer); + } +} + impl SseEncode for std::collections::HashMap> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -5953,6 +6606,24 @@ impl SseEncode for isize { } } +impl SseEncode for crate::user_config::LastBackupUploadState { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::user_config::LastBackupUploadState::None => 0, + crate::user_config::LastBackupUploadState::Pending => 1, + crate::user_config::LastBackupUploadState::Failed => 2, + crate::user_config::LastBackupUploadState::Success => 3, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + impl SseEncode for crate::bridge::wrapper::app_database::LegacyMigrationReport { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6088,6 +6759,16 @@ impl SseEncode for Vec<(i64, Vec)> { } } +impl SseEncode for Vec<(String, Vec)> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + <(String, Vec)>::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6108,6 +6789,16 @@ impl SseEncode for Vec { } } +impl SseEncode for Option>> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + >>::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6118,6 +6809,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + >::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6168,6 +6869,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6178,6 +6889,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6188,6 +6909,26 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + >::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6198,6 +6939,22 @@ impl SseEncode for Option> { } } +impl SseEncode for crate::user_config::PasswordlessRecoveryConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.email, serializer); + ::sse_encode(self.threshold, serializer); + >>::sse_encode(self.server_key_protection, serializer); + >>::sse_encode(self.pin_unlock_token, serializer); + >>::sse_encode(self.last_server_heartbeat, serializer); + >>::sse_encode( + self.last_contact_heartbeat, + serializer, + ); + >>::sse_encode(self.encrypted_server_key, serializer); + } +} + impl SseEncode for crate::api::server::prekeys::PqcPreKeyInput { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6233,6 +6990,14 @@ impl SseEncode for (Vec, i64) { } } +impl SseEncode for (String, Vec) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + >::sse_encode(self.1, serializer); + } +} + impl SseEncode for (String, String) { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6276,59 +7041,19 @@ impl SseEncode for crate::bridge::wrapper::RustUtils { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} } -impl SseEncode for crate::bridge::api::ServerResultEmpty { +impl SseEncode for crate::user_config::SetupProfile { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - match self { - crate::bridge::api::ServerResultEmpty::Ok => { - ::sse_encode(0, serializer); - } - crate::bridge::api::ServerResultEmpty::ErrorCode(field0) => { - ::sse_encode(1, serializer); - ::sse_encode(field0, serializer); - } - _ => { - unimplemented!(""); - } - } - } -} - -impl SseEncode for crate::bridge::api::ServerResultI64 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - match self { - crate::bridge::api::ServerResultI64::Ok(field0) => { - ::sse_encode(0, serializer); - ::sse_encode(field0, serializer); - } - crate::bridge::api::ServerResultI64::ErrorCode(field0) => { - ::sse_encode(1, serializer); - ::sse_encode(field0, serializer); - } - _ => { - unimplemented!(""); - } - } - } -} - -impl SseEncode for crate::bridge::api::ServerResultVecU8 { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - match self { - crate::bridge::api::ServerResultVecU8::Ok(field0) => { - ::sse_encode(0, serializer); - >::sse_encode(field0, serializer); - } - crate::bridge::api::ServerResultVecU8::ErrorCode(field0) => { - ::sse_encode(1, serializer); - ::sse_encode(field0, serializer); - } - _ => { - unimplemented!(""); - } - } + ::sse_encode( + match self { + crate::user_config::SetupProfile::Standard => 0, + crate::user_config::SetupProfile::Customized => 1, + _ => { + unimplemented!(""); + } + }, + serializer, + ); } } @@ -6366,6 +7091,37 @@ impl SseEncode for crate::database::app::SqlValue { } } +impl SseEncode for crate::user_config::ThemeMode { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::user_config::ThemeMode::System => 0, + crate::user_config::ThemeMode::Light => 1, + crate::user_config::ThemeMode::Dark => 2, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::user_config::TwonlySafeBackup { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.last_backup_size, serializer); + ::sse_encode( + self.backup_upload_state, + serializer, + ); + >>::sse_encode(self.last_backup_done, serializer); + >::sse_encode(self.backup_id, serializer); + >::sse_encode(self.encryption_key, serializer); + } +} + impl SseEncode for u32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6405,6 +7161,93 @@ impl SseEncode for () { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} } +impl SseEncode for crate::user_config::UserConfig { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.user_id, serializer); + ::sse_encode(self.username, serializer); + ::sse_encode(self.display_name, serializer); + >::sse_encode(self.avatar_svg, serializer); + ::sse_encode(self.app_version, serializer); + ::sse_encode(self.avatar_counter, serializer); + ::sse_encode(self.video_stabilization_enabled, serializer); + ::sse_encode(self.is_developer, serializer); + ::sse_encode(self.device_id, serializer); + ::sse_encode(self.setup_profile, serializer); + ::sse_encode(self.subscription_plan, serializer); + >::sse_encode(self.subscription_plan_id_store, serializer); + >>::sse_encode(self.last_image_send, serializer); + >::sse_encode(self.todays_image_counter, serializer); + >::sse_encode(self.last_plan_ballance, serializer); + >::sse_encode(self.additional_user_invites, serializer); + ::sse_encode(self.theme_mode, serializer); + >::sse_encode(self.primary_color_value, serializer); + >::sse_encode(self.default_show_time, serializer); + ::sse_encode(self.requested_audio_permission, serializer); + ::sse_encode(self.enable_database_logging, serializer); + ::sse_encode( + self.automatically_mark_equal_media_files_as_opened, + serializer, + ); + ::sse_encode(self.show_news_shortcut, serializer); + ::sse_encode(self.show_show_image_preview_when_sending, serializer); + ::sse_encode(self.start_with_camera_open, serializer); + >>::sse_encode(self.pre_selected_emojies, serializer); + >>>::sse_encode( + self.auto_download_options, + serializer, + ); + ::sse_encode(self.store_media_files_in_gallery, serializer); + ::sse_encode(self.auto_store_all_send_unlimited_media_files, serializer); + ::sse_encode(self.typing_indicators, serializer); + ::sse_encode(self.show_restore_flame, serializer); + >::sse_encode(self.my_best_friend_group_id, serializer); + >>::sse_encode( + self.signal_last_signed_pre_key_updated, + serializer, + ); + >>::sse_encode( + self.signal_last_pqc_pre_keys_uploaded, + serializer, + ); + ::sse_encode(self.allow_error_tracking_via_sentry, serializer); + ::sse_encode(self.screen_lock_enabled, serializer); + ::sse_encode(self.is_cloud_backup_enabled, serializer); + ::sse_encode(self.is_user_discovery_enabled, serializer); + ::sse_encode(self.required_send_images, serializer); + ::sse_encode(self.user_discovery_threshold, serializer); + ::sse_encode(self.user_discovery_requires_manual_approval, serializer); + ::sse_encode(self.user_discovery_share_promotion, serializer); + ::sse_encode(self.user_discovery_initialization_error, serializer); + ::sse_encode(self.ask_for_friend_promotions, serializer); + ::sse_encode(self.current_pre_key_index_start, serializer); + ::sse_encode(self.current_signed_pre_key_index_start, serializer); + >>::sse_encode(self.last_change_log_hash, serializer); + ::sse_encode(self.hide_change_log, serializer); + ::sse_encode(self.hide_memories_backup_promo, serializer); + ::sse_encode(self.update_fcm_token, serializer); + ::sse_encode(self.can_use_login_token_for_auth, serializer); + >::sse_encode( + self.twonly_safe_backup, + serializer, + ); + ::sse_encode(self.is_backup_enabled, serializer); + >::sse_encode( + self.password_less_recovery, + serializer, + ); + >::sse_encode(self.fcm_token, serializer); + >::sse_encode(self.current_setup_page, serializer); + ::sse_encode(self.skip_setup_pages, serializer); + ::sse_encode(self.has_zoomed, serializer); + } +} + +impl SseEncode for crate::bridge::user_config::UserConfigApi { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + impl SseEncode for usize { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 066d70d6..d8c9daee 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -12,7 +12,6 @@ mod error; pub use error::TwonlyError; mod frb_generated; mod keys; -#[doc(hidden)] pub mod log; pub mod sealed_sender; mod secure_storage; diff --git a/rust/src/user_discovery/types.proto b/rust/src/models/user_discovery.proto similarity index 100% rename from rust/src/user_discovery/types.proto rename to rust/src/models/user_discovery.proto diff --git a/rust/src/services/contacts.rs b/rust/src/services/contacts.rs index c6830dce..ce6ea8f5 100644 --- a/rust/src/services/contacts.rs +++ b/rust/src/services/contacts.rs @@ -198,8 +198,8 @@ impl ContactService { proto::EncryptedContent { contact_update: Some(encrypted_content::ContactUpdate { r#type: encrypted_content::contact_update::Type::Update as i32, - username: config.username, - display_name: config.display_name, + username: Some(config.username), + display_name: Some(config.display_name), avatar_svg_compressed, }), ..Default::default() diff --git a/rust/src/services/groups/mod.rs b/rust/src/services/groups/mod.rs index d127a370..3542ce8d 100644 --- a/rust/src/services/groups/mod.rs +++ b/rust/src/services/groups/mod.rs @@ -7,7 +7,9 @@ pub(crate) mod crypto; pub(crate) mod model; use crate::api::groups::GroupApi; +use crate::api::messages::incoming::client2client::messages::queue_encrypted_content; use crate::api::messages::outgoing::send_c2c_message_to_contact; +use crate::api::proto::client::encrypted_content::GroupJoin; use crate::api::proto::client::{ encrypted_appended_group_state, encrypted_content, EncryptedAppendedGroupState, EncryptedContent, EncryptedGroupState, @@ -22,9 +24,9 @@ use crate::database::app::tables::{ }; use crate::error::{Result, TwonlyError}; use crate::services::messages::MessageService; -use crate::utils::new_uuid_v4; +use crate::utils::{current_time, new_uuid_v4}; use model::GroupRecord; -use prost::Message as _; +use prost::Message; use rand::{RngCore, SeedableRng}; use std::collections::BTreeSet; use std::sync::Arc; @@ -40,6 +42,61 @@ impl GroupService { Self { ctx: ctx.clone() } } + pub(crate) async fn handle_membership_error( + &self, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + from_user_id: i64, + group_id: String, + related_receipt_id: String, + ) -> Result<()> { + if !self.fetch_group_state_in_transaction(t, &group_id).await? { + return Ok(()); + } + + let group = GroupRecord::load_in_transaction(t, &group_id).await?; + let is_still_member = sqlx::query_scalar!( + r#"SELECT EXISTS( + SELECT 1 FROM group_members + WHERE group_id = ? AND contact_id = ? AND member_state != 'leftGroup' + )"#, + group_id, + from_user_id, + ) + .fetch_one(&mut **t) + .await? + != 0; + + if is_still_member { + queue_encrypted_content( + t, + from_user_id, + EncryptedContent { + group_id: Some(group_id.clone()), + group_create: Some(encrypted_content::GroupCreate { + state_key: group.state_key()?.to_vec(), + group_public_key: group.identity()?.identity_key().serialize().to_vec(), + group_name: None, + }), + ..Default::default() + }, + true, + ) + .await?; + } + + sqlx::query!( + r#"UPDATE receipts + SET mark_for_retry = ?, retry_count = retry_count + 1 + WHERE receipt_id = ? AND contact_id = ?"#, + current_time().timestamp(), + related_receipt_id, + from_user_id, + ) + .execute(&mut **t) + .await?; + Ok(()) + } + pub async fn create_group(&self, group_name: String, member_ids: Vec) -> Result { let local = self.ctx.user_id().await?; let group_id = new_uuid_v4(); @@ -112,17 +169,28 @@ impl GroupService { pub async fn fetch_group_state(&self, group_id: String) -> Result { let database = self.ctx.get_app_database().await; - let group = GroupRecord::load(&database.pool, &group_id).await?; + let mut t = database.pool.begin().await?; + let updated = self + .fetch_group_state_in_transaction(&mut t, &group_id) + .await?; + t.commit().await?; + database.notify_committed(["groups", "group_members", "contacts"]); + Ok(updated) + } + + async fn fetch_group_state_in_transaction( + &self, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + group_id: &str, + ) -> Result { + let group = GroupRecord::load_in_transaction(t, group_id).await?; let Some(server) = GroupApi::fetch_group_state(&group_id).await? else { - let mut tr = database.pool.begin().await?; UpdateGroup::builder() - .group_id(group_id.clone()) + .group_id(group_id.to_owned()) .left_group(true) .build() - .execute(&mut tr) + .execute(t) .await?; - tr.commit().await?; - database.notify_committed(["groups"]); return Ok(false); }; let raw = crypto::decrypt(group.state_key()?, &server.encrypted_group_state)?; @@ -157,7 +225,7 @@ impl GroupService { group_id, tbs.public_key, ) - .fetch_optional(&database.pool) + .fetch_optional(&mut **t) .await? }; if let Some(leaving_id) = leaving_id { @@ -169,14 +237,8 @@ impl GroupService { if appended_changes && group_state.admin_ids.contains(&self.ctx.user_id().await?) { GroupApi::update_remote(&group, server.version_id, &group_state, None, None).await?; } - self.apply_state( - &database.pool, - &group_id, - server.version_id as i64, - &group_state, - ) - .await?; - database.notify_committed(["groups", "group_members", "contacts"]); + self.apply_state(t, group_id, server.version_id as i64, &group_state) + .await?; Ok(true) } @@ -414,14 +476,81 @@ impl GroupService { Ok(true) } + pub async fn refresh_group_state( + &self, + tr: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + group_id: String, + created: bool, + ) { + if created { + let _ = self + .fetch_group_states_for_unjoined_groups_in_transaction(tr) + .await; + let _ = self.broadcast_group_public_key(tr, &group_id).await; + } else { + let _ = self.fetch_group_state_in_transaction(tr, &group_id).await; + } + } + + pub async fn broadcast_group_public_key( + &self, + tr: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + group_id: &str, + ) -> Result<()> { + let group = sqlx::query!( + "SELECT my_group_private_key FROM groups WHERE group_id = ?", + group_id + ) + .fetch_optional(&mut **tr) + .await + .ok() + .flatten(); + + if let Some(record) = group { + if let Some(private_key) = record.my_group_private_key { + if let Ok(identity) = + libsignal_protocol::IdentityKeyPair::try_from(private_key.as_slice()) + { + let content = EncryptedContent { + group_join: Some(GroupJoin { + group_public_key: identity.identity_key().serialize().to_vec(), + }), + ..Default::default() + }; + let _ = MessageService::new(&self.ctx) + .send_to_group_in_transaction( + tr, + group_id.to_string(), + content.encode_to_vec(), + None, + false, + ) + .await; + } + } + } + Ok(()) + } + pub async fn fetch_group_states_for_unjoined_groups(&self) -> Result<()> { let db = self.ctx.get_app_database().await; + let mut t = db.pool.begin().await?; + self.fetch_group_states_for_unjoined_groups_in_transaction(&mut t) + .await?; + t.commit().await?; + Ok(()) + } + + pub async fn fetch_group_states_for_unjoined_groups_in_transaction( + &self, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + ) -> Result<()> { let ids = GetUnjoinedGroups::builder() .build() - .fetch_all(&db.pool) + .fetch_all(&mut **t) .await?; for id in ids { - if let Err(e) = self.fetch_group_state(id.clone()).await { + if let Err(e) = self.fetch_group_state_in_transaction(t, &id).await { tracing::warn!(group_id = id, "group state refresh failed: {e}") } } @@ -491,7 +620,7 @@ impl GroupService { } async fn history( - tr: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, group_id: &str, kind: &str, affected: Option, @@ -505,14 +634,14 @@ impl GroupService { .maybe_new_group_name(name.map(|n| n.to_string())) .maybe_new_delete_messages_after_milliseconds(delete_ms) .build() - .execute(tr) + .execute(t) .await?; Ok(()) } async fn apply_state( &self, - pool: &sqlx::SqlitePool, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, group_id: &str, version: i64, group_state: &EncryptedGroupState, @@ -521,8 +650,6 @@ impl GroupService { let joined = group_state.member_ids.contains(&local_user_id); let admin = group_state.admin_ids.contains(&local_user_id); - let mut tr = pool.begin().await?; - UpdateGroup::builder() .group_id(group_id.to_string()) .group_name(group_state.group_name.clone()) @@ -532,14 +659,14 @@ impl GroupService { .left_group(!joined) .state_version_id(version as u64) .build() - .execute(&mut tr) + .execute(t) .await?; UpdateGroupMemberState::builder() .group_id(group_id.to_string()) .member_state("leftGroup".into()) .build() - .execute(&mut tr) + .execute(t) .await?; for &contact_id in &group_state.member_ids { @@ -547,7 +674,7 @@ impl GroupService { continue; } - let exists = Contact::exists(&mut tr, contact_id).await?; + let exists = Contact::exists(t, contact_id).await?; if !exists { let user = match Server::get_user_by_id(&self.ctx, contact_id).await? { @@ -567,7 +694,7 @@ impl GroupService { .username(username) .deleted_by_user(true) .build() - .insert_on_conflict_update(&mut tr) + .insert_on_conflict_update(t) .await?; } @@ -583,11 +710,9 @@ impl GroupService { .member_state(member_state.into()) .on_conflict_update(true) .build() - .execute(&mut tr) + .execute(t) .await?; } - - tr.commit().await?; Ok(()) } } diff --git a/rust/src/services/groups/model.rs b/rust/src/services/groups/model.rs index 397b1b6b..8947018f 100644 --- a/rust/src/services/groups/model.rs +++ b/rust/src/services/groups/model.rs @@ -3,7 +3,7 @@ * */ -use sqlx::SqlitePool; +use sqlx::{Sqlite, SqlitePool, Transaction}; use crate::error::{Result, TwonlyError}; @@ -30,6 +30,22 @@ impl GroupRecord { .ok_or_else(|| TwonlyError::Generic(format!("group {group_id} does not exist"))) } + pub async fn load_in_transaction( + transaction: &mut Transaction<'_, Sqlite>, + group_id: &str, + ) -> Result { + sqlx::query_as!( + Self, + r#"SELECT group_id, group_name, state_version_id, + state_encryption_key, my_group_private_key + FROM groups WHERE group_id = ?"#, + group_id, + ) + .fetch_optional(&mut **transaction) + .await? + .ok_or_else(|| TwonlyError::Generic(format!("group {group_id} does not exist"))) + } + pub fn state_key(&self) -> Result<&[u8]> { self.state_encryption_key .as_deref() diff --git a/rust/src/services/mediafiles.rs b/rust/src/services/mediafiles.rs new file mode 100644 index 00000000..5d8a79b3 --- /dev/null +++ b/rust/src/services/mediafiles.rs @@ -0,0 +1,392 @@ +/* + * Copyright (c) 2026, Tobias Müller git@tsmr.eu + * + */ + +use crate::api::proto::client::{encrypted_content, EncryptedContent}; +use crate::bridge::api::RustApi; +use crate::context::Context; +use crate::error::{Result, TwonlyError}; +use chacha20poly1305::aead::{AeadInPlace, KeyInit}; +use chacha20poly1305::{ChaCha20Poly1305, Nonce, Tag}; +use prost::Message as _; +use sha2::{Digest, Sha256}; +use sqlx::FromRow; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; + +#[derive(FromRow)] +struct DownloadMedia { + media_id: String, + media_type: String, + download_token: Option>, + encryption_key: Option>, + encryption_mac: Option>, + encryption_nonce: Option>, +} + +#[derive(FromRow)] +struct ReuploadTarget { + message_id: String, + sender_id: i64, +} + +/// Filesystem and download operations for media files. Upload preparation and +/// platform-specific media processing intentionally remain in Flutter. +pub struct MediaFileService { + ctx: Arc, +} + +impl MediaFileService { + pub fn new(ctx: &Arc) -> Self { + Self { ctx: ctx.clone() } + } + + pub async fn download_pending(&self) -> Result<()> { + let database = self.ctx.get_app_database().await; + // A terminated process can leave a download in this intermediate state. + sqlx::query!( + "UPDATE media_files SET download_state = 'pending' WHERE download_state = 'downloading'" + ) + .execute(&database.pool) + .await?; + let media_ids = sqlx::query_scalar!( + "SELECT media_id FROM media_files WHERE download_state = 'pending'" + ) + .fetch_all(&database.pool) + .await?; + drop(database); + + for media_id in media_ids { + if let Err(error) = self.download(&media_id).await { + tracing::warn!(media_id, %error, "media download failed"); + } + } + Ok(()) + } + + pub async fn download(&self, media_id: &str) -> Result<()> { + let database = self.ctx.get_app_database().await; + let claimed = sqlx::query!( + r#"UPDATE media_files SET download_state = 'downloading' + WHERE media_id = ? AND download_state = 'pending'"#, + media_id, + ) + .execute(&database.pool) + .await? + .rows_affected(); + if claimed == 0 { + return Ok(()); + } + + let result = self.download_claimed(media_id).await; + if result.is_err() { + sqlx::query!( + r#"UPDATE media_files SET download_state = 'pending' + WHERE media_id = ? AND download_state = 'downloading'"#, + media_id, + ) + .execute(&database.pool) + .await?; + database.notify_committed(["media_files"]); + } + result + } + + /// Incoming media is inserted inside a transaction owned by the message + /// handler. Wait until that transaction becomes visible before claiming it. + pub async fn download_when_available(&self, media_id: &str) -> Result<()> { + for _ in 0..40 { + let database = self.ctx.get_app_database().await; + let state = sqlx::query_scalar!( + "SELECT download_state FROM media_files WHERE media_id = ?", + media_id, + ) + .fetch_optional(&database.pool) + .await? + .flatten(); + match state.as_deref() { + Some("pending") => return self.download(media_id).await, + Some(_) => return Ok(()), + None => tokio::time::sleep(Duration::from_millis(50)).await, + } + } + Err(TwonlyError::Generic(format!( + "media {media_id} was not committed in time" + ))) + } + + async fn download_claimed(&self, media_id: &str) -> Result<()> { + let database = self.ctx.get_app_database().await; + let messages = sqlx::query!( + r#"SELECT m.message_id, m.sender_id, c.account_deleted + FROM messages m + LEFT JOIN contacts c ON c.user_id = m.sender_id + WHERE m.media_id = ?"#, + media_id, + ) + .fetch_all(&database.pool) + .await?; + + if messages.is_empty() { + let media_type = + sqlx::query_scalar!("SELECT type FROM media_files WHERE media_id = ?", media_id,) + .fetch_optional(&database.pool) + .await?; + sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id) + .execute(&database.pool) + .await?; + if let Some(media_type) = media_type { + self.remove_files(media_id, &media_type)?; + } + database.notify_committed(["media_files"]); + return Ok(()); + } + if messages.len() != 1 { + return Err(TwonlyError::Generic(format!( + "media {media_id} has {} original messages", + messages.len() + ))); + } + let message = &messages[0]; + if message.sender_id.is_none() { + return Err(TwonlyError::Generic(format!( + "media {media_id} has no sender" + ))); + } + if message.account_deleted.unwrap_or(1) != 0 { + let media_type = + sqlx::query_scalar!("SELECT type FROM media_files WHERE media_id = ?", media_id,) + .fetch_optional(&database.pool) + .await?; + sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id) + .execute(&database.pool) + .await?; + sqlx::query!( + "DELETE FROM messages WHERE message_id = ?", + message.message_id + ) + .execute(&database.pool) + .await?; + if let Some(media_type) = media_type { + self.remove_files(media_id, &media_type)?; + } + database.notify_committed(["messages", "media_files"]); + return Ok(()); + } + + let media = sqlx::query_as::<_, DownloadMedia>( + r#"SELECT media_id, type AS media_type, download_token, + encryption_key, encryption_mac, encryption_nonce + FROM media_files WHERE media_id = ?"#, + ) + .bind(media_id) + .fetch_optional(&database.pool) + .await? + .ok_or_else(|| TwonlyError::Generic(format!("media {media_id} not found")))?; + + let encrypted_path = self.encrypted_path(&media.media_id, &media.media_type); + if !encrypted_path.exists() { + let token = media.download_token.as_deref().ok_or_else(|| { + TwonlyError::Generic(format!("media {media_id} has no download token")) + })?; + let url = format!( + "{}download/{}", + RustApi::api_base_url("https".into()), + hex::encode(token) + ); + let response = reqwest::Client::new() + .get(url) + .timeout(Duration::from_secs(30)) + .send() + .await + .map_err(|error| TwonlyError::Generic(error.to_string()))?; + let status = response.status(); + if status == reqwest::StatusCode::NOT_FOUND || status == reqwest::StatusCode::FORBIDDEN + { + self.request_reupload(media_id).await?; + return Ok(()); + } + if !status.is_success() { + return Err(TwonlyError::Generic(format!( + "media download returned HTTP {status}" + ))); + } + let bytes = response + .bytes() + .await + .map_err(|error| TwonlyError::Generic(error.to_string()))?; + Self::ensure_parent(&encrypted_path)?; + std::fs::write(&encrypted_path, bytes)?; + } + + if let Err(error) = self.decrypt(&media, &encrypted_path).await { + tracing::warn!(media_id, %error, "media decryption failed; requesting reupload"); + self.request_reupload(media_id).await?; + return Ok(()); + } + database.notify_committed(["media_files"]); + Ok(()) + } + + async fn decrypt(&self, media: &DownloadMedia, encrypted_path: &Path) -> Result<()> { + let key = media.encryption_key.as_deref().ok_or_else(|| { + TwonlyError::Generic(format!("media {} has no encryption key", media.media_id)) + })?; + let nonce = media.encryption_nonce.as_deref().ok_or_else(|| { + TwonlyError::Generic(format!("media {} has no encryption nonce", media.media_id)) + })?; + let mac = media.encryption_mac.as_deref().ok_or_else(|| { + TwonlyError::Generic(format!("media {} has no encryption MAC", media.media_id)) + })?; + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|_| TwonlyError::Generic("invalid media encryption key".into()))?; + if nonce.len() != 12 { + return Err(TwonlyError::Generic( + "invalid media encryption nonce".into(), + )); + } + if mac.len() != 16 { + return Err(TwonlyError::Generic("invalid media encryption MAC".into())); + } + let nonce = Nonce::from_slice(nonce); + let tag = Tag::from_slice(mac); + let mut bytes = std::fs::read(encrypted_path)?; + cipher + .decrypt_in_place_detached(nonce, b"", &mut bytes, tag) + .map_err(|_| TwonlyError::Generic("media authentication failed".into()))?; + + let temp_path = self.temp_path(&media.media_id, &media.media_type); + Self::ensure_parent(&temp_path)?; + std::fs::write(&temp_path, &bytes)?; + let hash = Sha256::digest(&bytes).to_vec(); + + // Keep the file update and state transition ordered: ready is only + // visible after the plaintext has been written successfully. + let database = self.ctx.get_app_database().await; + sqlx::query!( + r#"UPDATE media_files SET download_state = 'ready', stored_file_hash = ? + WHERE media_id = ?"#, + hash, + media.media_id, + ) + .execute(&database.pool) + .await?; + std::fs::remove_file(encrypted_path)?; + Ok(()) + } + + pub async fn request_reupload(&self, media_id: &str) -> Result<()> { + let database = self.ctx.get_app_database().await; + sqlx::query!( + "UPDATE media_files SET download_state = 'reuploadRequested' WHERE media_id = ?", + media_id, + ) + .execute(&database.pool) + .await?; + let targets = sqlx::query_as::<_, ReuploadTarget>( + r#"SELECT message_id, sender_id FROM messages + WHERE media_id = ? AND opened_at IS NULL AND sender_id IS NOT NULL"#, + ) + .bind(media_id) + .fetch_all(&database.pool) + .await?; + database.notify_committed(["media_files"]); + drop(database); + + for target in targets { + let content = EncryptedContent { + media_update: Some(encrypted_content::MediaUpdate { + r#type: encrypted_content::media_update::Type::DecryptionError.into(), + target_message_id: target.message_id, + }), + ..Default::default() + }; + crate::api::messages::outgoing::send_c2c_message_to_contact() + .ctx(&self.ctx) + .contact_id(target.sender_id) + .encrypted_content(content.encode_to_vec()) + .call() + .await?; + } + Ok(()) + } + + pub fn remove_files(&self, media_id: &str, media_type: &str) -> Result<()> { + for path in self.paths(media_id, media_type) { + match std::fs::remove_file(path) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(error.into()), + } + } + Ok(()) + } + + pub async fn remove_files_if_deleted(&self, media_id: &str, media_type: &str) -> Result<()> { + let database = self.ctx.get_app_database().await; + let still_exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM media_files WHERE media_id = ?)", + media_id, + ) + .fetch_one(&database.pool) + .await? + != 0; + if !still_exists { + self.remove_files(media_id, media_type)?; + } + Ok(()) + } + + fn paths(&self, media_id: &str, media_type: &str) -> Vec { + let extension = Self::extension(media_type); + let base = PathBuf::from(self.ctx.data_dir()).join("mediafiles"); + vec![ + base.join("tmp").join(format!("{media_id}.{extension}")), + base.join("tmp") + .join(format!("{media_id}.encrypted.{extension}")), + base.join("tmp") + .join(format!("{media_id}.original.{extension}")), + base.join("tmp") + .join(format!("{media_id}.upload.{extension}")), + base.join("tmp") + .join(format!("{media_id}.ffmpeg.{extension}")), + base.join("tmp").join(format!("{media_id}.overlay.png")), + base.join("stored").join(format!("{media_id}.{extension}")), + base.join("stored") + .join(format!("{media_id}.thumbnail.webp")), + ] + } + + fn temp_path(&self, media_id: &str, media_type: &str) -> PathBuf { + PathBuf::from(self.ctx.data_dir()) + .join("mediafiles/tmp") + .join(format!("{media_id}.{}", Self::extension(media_type))) + } + + fn encrypted_path(&self, media_id: &str, media_type: &str) -> PathBuf { + PathBuf::from(self.ctx.data_dir()) + .join("mediafiles/tmp") + .join(format!( + "{media_id}.encrypted.{}", + Self::extension(media_type) + )) + } + + fn extension(media_type: &str) -> &'static str { + match media_type { + "video" => "mp4", + "gif" => "gif", + "audio" => "m4a", + _ => "webp", + } + } + + fn ensure_parent(path: &Path) -> Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + Ok(()) + } +} diff --git a/rust/src/services/messages.rs b/rust/src/services/messages.rs index 6e4210fb..6e23317c 100644 --- a/rust/src/services/messages.rs +++ b/rust/src/services/messages.rs @@ -3,8 +3,10 @@ * */ -use crate::api::messages::incoming::client2client::messages; -use crate::api::messages::outgoing::send_c2c_message_to_contact; +use crate::api::messages::incoming::client2client::messages::{ + self, queue_encrypted_content, send_queued_receipt, +}; +use crate::api::messages::outgoing::{decorate_content, send_c2c_message_to_contact}; use crate::api::proto::client::{self as proto, encrypted_content}; use crate::context::Context; use crate::database::app::tables::{Contact, Group}; @@ -62,6 +64,110 @@ impl MessageService { Ok(()) } + pub async fn send_to_group_in_transaction( + &self, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + group_id: String, + encrypted_content: Vec, + message_id: Option, + only_send_if_no_receipts_are_open: bool, + ) -> Result<()> { + let mut content = proto::EncryptedContent::decode(encrypted_content.as_slice())?; + content.group_id = Some(group_id.clone()); + if message_id.is_some() + || content.reaction.is_some() + || content.media.is_some() + || content.text_message.is_some() + { + sqlx::query!("UPDATE groups SET last_message_exchange = CAST(strftime('%s','now') AS INTEGER) WHERE group_id = ?", group_id) + .execute(&mut **t).await?; + } + let members = sqlx::query_scalar!( + r#"SELECT contact_id FROM group_members + WHERE group_id = ? AND (member_state IS NULL OR member_state != 'leftGroup')"#, + group_id, + ) + .fetch_all(&mut **t) + .await?; + + let bytes = content.encode_to_vec(); + let mut receipt_ids = Vec::new(); + + for contact_id in members { + let mut contact_content = proto::EncryptedContent::decode(bytes.as_slice())?; + decorate_content( + &self.ctx, + contact_id, + &mut contact_content, + message_id.is_some(), + ) + .await?; + + if only_send_if_no_receipts_are_open { + let count = sqlx::query_scalar!( + "SELECT COUNT(*) FROM receipts WHERE contact_id = ?", + contact_id + ) + .fetch_one(&mut **t) + .await?; + if count > 10 { + continue; + } + } + + let mut retry_count = 0_i64; + let mut last_retry = None; + if let Some(msg_id) = &message_id { + let previous = sqlx::query!( + r#"SELECT COUNT(*) AS "count!: i64", MAX(last_retry) AS last_retry + FROM receipts WHERE contact_id = ? AND message_id = ?"#, + contact_id, + msg_id, + ) + .fetch_one(&mut **t) + .await?; + retry_count = previous.count; + last_retry = previous.last_retry; + sqlx::query!( + "DELETE FROM receipts WHERE contact_id = ? AND message_id = ?", + contact_id, + msg_id + ) + .execute(&mut **t) + .await?; + } + + let receipt_id = queue_encrypted_content(t, contact_id, contact_content, true).await?; + + sqlx::query!( + r#"UPDATE receipts SET message_id = ?, will_be_retried_by_media_upload = ?, + retry_count = ?, last_retry = ? WHERE receipt_id = ?"#, + message_id, + false, // only_return_encrypted_data is false for send_to_group + retry_count, + last_retry, + receipt_id + ) + .execute(&mut **t) + .await?; + + receipt_ids.push(receipt_id); + } + + // We CANNOT spawn the sending here immediately because the transaction is NOT committed yet! + for receipt_id in receipt_ids { + let ctx = self.ctx.clone(); + tokio::spawn(async move { + // Sleep slightly to let the transaction commit + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + if let Err(error) = send_queued_receipt(&ctx, &receipt_id).await { + tracing::warn!(receipt_id, "queued group message send failed: {error}"); + } + }); + } + Ok(()) + } + pub async fn insert_and_send_text( &self, group_id: String, diff --git a/rust/src/services/mod.rs b/rust/src/services/mod.rs index 1031d4c2..207ca8dd 100644 --- a/rust/src/services/mod.rs +++ b/rust/src/services/mod.rs @@ -5,4 +5,5 @@ pub mod contacts; pub mod groups; +pub mod mediafiles; pub mod messages; diff --git a/rust/src/user_config.rs b/rust/src/user_config.rs index 0f3cbc74..4e64eb9b 100644 --- a/rust/src/user_config.rs +++ b/rust/src/user_config.rs @@ -3,68 +3,354 @@ * */ +use crate::context::Context; +use crate::error::{twonly_error, Result, TwonlyError}; use serde::{Deserialize, Serialize}; -use std::{fs::File, path::PathBuf}; +use std::collections::HashMap; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; +use std::sync::{OnceLock, RwLock}; -use crate::{ - context::Context, - error::{twonly_error, Result, TwonlyError}, -}; +mod defaults { + pub fn true_value() -> bool { + true + } -fn default_required_send_images() -> i64 { - 4 + pub fn subscription_plan() -> String { + "Free".into() + } + + pub fn pre_key_index() -> i64 { + 100_000 + } + + pub fn required_send_images() -> i64 { + 4 + } + + pub fn user_discovery_threshold() -> u8 { + 3 + } + + pub fn passwordless_threshold() -> i64 { + 2 + } } -fn default_can_use_login_token_for_auth() -> bool { - true +mod optional_datetime { + use chrono::{DateTime, NaiveDateTime, Utc}; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &Option>, serializer: S) -> Result + where + S: Serializer, + { + match value { + Some(date) => serializer.serialize_some(&date.to_rfc3339()), + None => serializer.serialize_none(), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + let Some(value) = Option::::deserialize(deserializer)? else { + return Ok(None); + }; + if let Ok(date) = DateTime::parse_from_rfc3339(&value) { + return Ok(Some(date.with_timezone(&Utc))); + } + NaiveDateTime::parse_from_str(&value, "%Y-%m-%dT%H:%M:%S%.f") + .map(|date| Some(date.and_utc())) + .map_err(serde::de::Error::custom) + } } -/// Read-only Rust view of the persisted `user.json` file. -/// -/// This deliberately contains only fields consumed by Rust. Unknown Dart -/// fields are ignored by Serde. Loading uses `File::open`, so this module can -/// never create, truncate, or modify the configuration file. -#[derive(Clone, Debug, Deserialize, Serialize, Default)] +fn config_lock() -> &'static RwLock<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| RwLock::new(())) +} + +fn merge_changed_fields( + current: &mut serde_json::Map, + base: &serde_json::Map, + updated: &serde_json::Map, +) { + for key in base.keys().chain(updated.keys()) { + if base.get(key) == updated.get(key) { + continue; + } + match updated.get(key) { + Some(value) => { + current.insert(key.clone(), value.clone()); + } + None => { + current.remove(key); + } + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq)] #[serde(rename_all = "camelCase")] -pub struct UserConfig { - pub user_id: Option, +#[flutter_rust_bridge::frb(non_final)] +pub struct TwonlySafeBackup { #[serde(default)] - pub device_id: i64, + #[frb(non_final)] + pub last_backup_size: i64, #[serde(default)] - pub app_version: i64, + #[frb(non_final)] + pub backup_upload_state: LastBackupUploadState, + #[serde(default, with = "optional_datetime")] + #[frb(non_final)] + pub last_backup_done: Option>, + #[serde(default)] + #[frb(non_final)] + pub backup_id: Vec, + #[serde(default)] + #[frb(non_final)] + pub encryption_key: Vec, +} - #[serde(default = "default_can_use_login_token_for_auth")] - pub can_use_login_token_for_auth: bool, - #[serde(default)] - pub is_user_discovery_enabled: bool, - #[serde(default = "default_required_send_images")] - pub required_send_images: i64, - #[serde(default)] - pub user_discovery_requires_manual_approval: bool, - pub username: Option, - pub display_name: Option, +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum LastBackupUploadState { + #[default] + None, + Pending, + Failed, + Success, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +#[flutter_rust_bridge::frb(non_final)] +pub struct PasswordlessRecoveryConfig { + #[frb(non_final)] + pub email: Option, + #[serde(default = "defaults::passwordless_threshold")] + #[frb(non_final)] + pub threshold: i64, + #[frb(non_final)] + pub server_key_protection: Option>, + #[frb(non_final)] + pub pin_unlock_token: Option>, + #[serde(default, with = "optional_datetime")] + #[frb(non_final)] + pub last_server_heartbeat: Option>, + #[serde(default, with = "optional_datetime")] + #[frb(non_final)] + pub last_contact_heartbeat: Option>, + #[frb(non_final)] + pub encrypted_server_key: Option>, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum SetupProfile { + #[default] + Standard, + Customized, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum ThemeMode { + #[default] + System, + Light, + Dark, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +#[flutter_rust_bridge::frb(non_final)] +pub struct UserConfig { + #[frb(non_final)] + pub user_id: i64, + #[frb(non_final)] + pub username: String, + #[frb(non_final)] + pub display_name: String, + #[frb(non_final)] pub avatar_svg: Option, #[serde(default)] + #[frb(non_final)] + pub app_version: i64, + #[serde(default)] + #[frb(non_final)] pub avatar_counter: i64, - #[serde(default = "default_true")] - pub ask_for_friend_promotions: bool, - #[serde(default = "default_true")] - pub typing_indicators: bool, -} + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub video_stabilization_enabled: bool, + #[serde(default)] + #[frb(non_final)] + pub is_developer: bool, + #[serde(default)] + #[frb(non_final)] + pub device_id: i64, + #[serde(default)] + #[frb(non_final)] + pub setup_profile: SetupProfile, -fn default_true() -> bool { - true + #[serde(default = "defaults::subscription_plan")] + #[frb(non_final)] + pub subscription_plan: String, + #[frb(non_final)] + pub subscription_plan_id_store: Option, + #[serde(default, with = "optional_datetime")] + #[frb(non_final)] + pub last_image_send: Option>, + #[frb(non_final)] + pub todays_image_counter: Option, + #[frb(non_final)] + pub last_plan_ballance: Option, + #[frb(non_final)] + pub additional_user_invites: Option, + + #[serde(default)] + #[frb(non_final)] + pub theme_mode: ThemeMode, + #[frb(non_final)] + pub primary_color_value: Option, + #[frb(non_final)] + pub default_show_time: Option, + #[serde(default)] + #[frb(non_final)] + pub requested_audio_permission: bool, + #[serde(default)] + #[frb(non_final)] + pub enable_database_logging: bool, + #[serde(default)] + #[frb(non_final)] + pub automatically_mark_equal_media_files_as_opened: bool, + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub show_news_shortcut: bool, + #[serde(default)] + #[frb(non_final)] + pub show_show_image_preview_when_sending: bool, + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub start_with_camera_open: bool, + #[frb(non_final)] + pub pre_selected_emojies: Option>, + #[frb(non_final)] + pub auto_download_options: Option>>, + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub store_media_files_in_gallery: bool, + #[serde(default)] + #[frb(non_final)] + pub auto_store_all_send_unlimited_media_files: bool, + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub typing_indicators: bool, + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub show_restore_flame: bool, + #[frb(non_final)] + pub my_best_friend_group_id: Option, + #[serde(default, with = "optional_datetime")] + #[frb(non_final)] + pub signal_last_signed_pre_key_updated: Option>, + #[serde(default, with = "optional_datetime")] + #[frb(non_final)] + pub signal_last_pqc_pre_keys_uploaded: Option>, + #[serde(default)] + #[frb(non_final)] + pub allow_error_tracking_via_sentry: bool, + #[serde(default)] + #[frb(non_final)] + pub screen_lock_enabled: bool, + #[serde(default)] + #[frb(non_final)] + pub is_cloud_backup_enabled: bool, + + #[serde(default)] + #[frb(non_final)] + pub is_user_discovery_enabled: bool, + #[serde(default = "defaults::required_send_images")] + #[frb(non_final)] + pub required_send_images: i64, + #[serde(default = "defaults::user_discovery_threshold")] + #[frb(non_final)] + pub user_discovery_threshold: u8, + #[serde(default)] + #[frb(non_final)] + pub user_discovery_requires_manual_approval: bool, + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub user_discovery_share_promotion: bool, + #[serde(default)] + #[frb(non_final)] + pub user_discovery_initialization_error: bool, + + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub ask_for_friend_promotions: bool, + #[serde(default = "defaults::pre_key_index")] + #[frb(non_final)] + pub current_pre_key_index_start: i64, + #[serde(default = "defaults::pre_key_index")] + #[frb(non_final)] + pub current_signed_pre_key_index_start: i64, + #[frb(non_final)] + pub last_change_log_hash: Option>, + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub hide_change_log: bool, + #[serde(default)] + #[frb(non_final)] + pub hide_memories_backup_promo: bool, + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub update_fcm_token: bool, + #[serde(default = "defaults::true_value")] + #[frb(non_final)] + pub can_use_login_token_for_auth: bool, + + #[frb(non_final)] + pub twonly_safe_backup: Option, + #[serde(default)] + #[frb(non_final)] + pub is_backup_enabled: bool, + #[frb(non_final)] + pub password_less_recovery: Option, + #[frb(non_final)] + pub fcm_token: Option, + #[frb(non_final)] + pub current_setup_page: Option, + #[serde(default)] + #[frb(non_final)] + pub skip_setup_pages: bool, + #[serde(default)] + #[frb(non_final)] + pub has_zoomed: bool, } impl UserConfig { + fn path(context: &Context) -> PathBuf { + PathBuf::from(context.data_dir()) + .join("keyvalue") + .join("user.json") + } + pub(crate) fn load_required_from(context: &Context) -> Result { Self::load_from(context)?.ok_or_else(|| twonly_error!("user configuration is unavailable")) } pub(crate) fn load_from(context: &Context) -> Result> { - let path = PathBuf::from(context.data_dir()) - .join("keyvalue") - .join("user.json"); + let _guard = config_lock() + .read() + .map_err(|_| twonly_error!("user configuration lock was poisoned"))?; + Self::load_from_unlocked(context) + } + + fn load_from_unlocked(context: &Context) -> Result> { + let path = Self::path(context); if !path.exists() { return Ok(None); } @@ -76,11 +362,61 @@ impl UserConfig { )) }) } + + /// Validates and atomically persists the complete user configuration. + /// Rust is the only writer of `user.json`. + pub(crate) fn save_json(context: &Context, json: &str) -> Result { + let _guard = config_lock() + .write() + .map_err(|_| twonly_error!("user configuration lock was poisoned"))?; + Self::save_json_unlocked(context, json) + } + + /// Applies only fields changed relative to the caller's original snapshot. + /// Concurrent updates from Flutter isolates or Rust therefore do not + /// overwrite unrelated fields with stale values. + pub(crate) fn update_json( + context: &Context, + base_json: &str, + updated_json: &str, + ) -> Result { + let base: serde_json::Map = serde_json::from_str(base_json)?; + let updated: serde_json::Map = + serde_json::from_str(updated_json)?; + let _guard = config_lock() + .write() + .map_err(|_| twonly_error!("user configuration lock was poisoned"))?; + let current = Self::load_from_unlocked(context)? + .ok_or_else(|| twonly_error!("user configuration is unavailable"))?; + let mut current: serde_json::Map = + serde_json::from_value(serde_json::to_value(current)?)?; + + merge_changed_fields(&mut current, &base, &updated); + Self::save_json_unlocked(context, &serde_json::to_string(¤t)?) + } + + fn save_json_unlocked(context: &Context, json: &str) -> Result { + let config: Self = serde_json::from_str(json).map_err(|error| { + TwonlyError::Generic(format!("invalid user configuration update: {error}")) + })?; + let normalized = serde_json::to_string(&config)?; + let path = Self::path(context); + let parent = path + .parent() + .ok_or_else(|| twonly_error!("user configuration path has no parent"))?; + std::fs::create_dir_all(parent)?; + let temporary = parent.join("user.json.tmp"); + let mut file = File::create(&temporary)?; + file.write_all(normalized.as_bytes())?; + file.sync_all()?; + std::fs::rename(&temporary, &path)?; + Ok(normalized) + } } #[cfg(test)] mod tests { - use super::UserConfig; + use super::{merge_changed_fields, UserConfig}; #[test] fn parses_only_rust_fields_and_applies_defaults() { @@ -94,11 +430,23 @@ mod tests { ) .unwrap(); - assert_eq!(config.user_id, Some(42)); - assert_eq!(config.username.as_deref(), Some("alice")); + assert_eq!(config.user_id, 42); + assert_eq!(config.username, "alice"); assert_eq!(config.required_send_images, 4); assert!(config.can_use_login_token_for_auth); assert!(!config.is_user_discovery_enabled); } + + #[test] + fn merges_only_fields_changed_by_a_stale_snapshot() { + let mut current = serde_json::from_str(r#"{"theme":"dark","counter":2}"#).unwrap(); + let base = serde_json::from_str(r#"{"theme":"light","counter":1}"#).unwrap(); + let updated = serde_json::from_str(r#"{"theme":"system","counter":1}"#).unwrap(); + + merge_changed_fields(&mut current, &base, &updated); + + assert_eq!(current["theme"], "system"); + assert_eq!(current["counter"], 2); + } } diff --git a/rust/src/user_discovery/mod.rs b/rust/src/user_discovery.rs similarity index 58% rename from rust/src/user_discovery/mod.rs rename to rust/src/user_discovery.rs index 181082d7..592dfe86 100644 --- a/rust/src/user_discovery/mod.rs +++ b/rust/src/user_discovery.rs @@ -3,26 +3,21 @@ * */ -pub mod error; -pub mod stores; -#[cfg(test)] -pub mod tests; -pub mod traits; - -#[cfg(test)] -use std::collections::HashMap; -use std::collections::{HashSet}; +use std::collections::HashSet; +use std::path::PathBuf; use std::sync::Arc; use blahaj::{Share, Sharks}; +use libsignal_protocol::{IdentityKey, IdentityKeyPair}; use prost::Message; +use rand::SeedableRng; use serde::{Deserialize, Serialize}; -use tokio::sync::Mutex; -use crate::user_discovery::error::{Result, UserDiscoveryError}; -use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryUtils}; +use tokio::sync::{Mutex, RwLock}; +use crate::database::signal::Database; +use crate::keys::KeyManager; +use crate::error::{Result, TwonlyError}; use crate::user_discovery::user_discovery_message::{UserDiscoveryAnnouncement, UserDiscoveryPromotion}; use crate::user_discovery::user_discovery_message::user_discovery_promotion::AnnouncementShareDecrypted; use crate::user_discovery::user_discovery_message::user_discovery_promotion::announcement_share_decrypted::SignedData; -pub use traits::UserDiscoveryStore; /// Type of the user id, this must be consistent with the user id defined in /// the types.proto @@ -30,6 +25,23 @@ pub type UserID = i64; include!(concat!(env!("OUT_DIR"), "/user_discovery.rs")); +#[derive(Clone, sqlx::FromRow)] +pub struct OtherPromotion { + pub promotion_id: u32, + pub public_id: i64, + pub from_contact_id: UserID, + pub threshold: u8, + pub announcement_share: Vec, + pub public_key_verified_timestamp: Option, +} + +#[derive(Clone, Hash, PartialEq, Eq, Debug)] +pub struct AnnouncedUser { + pub user_id: UserID, + pub public_key: Vec, + pub public_id: i64, +} + #[derive(Serialize, Deserialize, Clone, Debug)] struct UserDiscoveryConfig { /// The number of required shares to get the secret @@ -50,31 +62,70 @@ struct UserDiscoveryConfig { share_promotion: bool, } -/// -/// The main struct to access the user discovery functionality. -/// -/// As generic values it requires a UserDiscoveryStore and a UserDiscoveryUtils. -/// -pub struct UserDiscovery -where - Store: UserDiscoveryStore, - Utils: UserDiscoveryUtils, -{ - store: Store, - utils: Utils, +pub struct UserDiscovery { + config_path: PathBuf, + key_manager: Arc>, + rust_db: Arc>>, config_lock: Arc>, } -impl UserDiscovery { - /// Creates a new instance of the user discovery. - pub fn new(store: Store, utils: Utils) -> Result { +impl UserDiscovery { + pub fn new( + data_dir: &str, + key_manager: Arc>, + rust_db: Arc>>, + ) -> Result { Ok(Self { - store, - utils, + config_path: PathBuf::from(data_dir).join("user_discovery_config.json"), + key_manager, + rust_db, config_lock: Arc::default(), }) } + async fn sign_data(&self, input_data: &[u8]) -> Result> { + let key_manager = self.key_manager.lock().await; + let identity = key_manager + .signal_identity + .as_ref() + .ok_or_else(|| TwonlyError::UserDiscoveryStore("no Signal identity found".into()))?; + let key_pair = IdentityKeyPair::try_from(identity.identity_key_pair_structure.as_slice()) + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?; + let mut csprng = rand::rngs::StdRng::from_os_rng(); + key_pair + .private_key() + .calculate_signature_for_multipart_message(&[input_data], &mut csprng) + .map(|signature| signature.to_vec()) + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string())) + } + + async fn verify_signature( + &self, + input_data: &[u8], + public_key: &[u8], + signature: &[u8], + ) -> Result { + let identity = match IdentityKey::decode(public_key) { + Ok(identity) => identity, + Err(_) => return Ok(false), + }; + Ok(identity + .public_key() + .verify_signature(input_data, signature)) + } + + async fn verify_stored_pubkey(&self, contact_id: UserID, public_key: &[u8]) -> Result { + let database = self.rust_db.read().await.clone(); + let stored = sqlx::query_scalar!( + "SELECT identity_key FROM signal_identities WHERE name = ?", + contact_id.to_string(), + ) + .fetch_optional(&database.pool) + .await + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?; + Ok(stored.as_deref() == Some(public_key)) + } + /// Initializes or updates the user discovery. /// /// This function will generate new verification shares and update the config. @@ -88,7 +139,7 @@ impl UserDiscovery UserDiscovery, share_promotion: bool, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, ) -> Result<()> { tracing::info!("Protocols: initialize_or_update started, getting config from store"); - let config = match self.store.get_config().await { + let config = match self.read_config() { Ok(config) => { let mut config: UserDiscoveryConfig = serde_json::from_str(&config)?; config.threshold = threshold; @@ -121,13 +173,13 @@ impl UserDiscovery UserDiscovery serde_json::from_str(&c)?, Err(_) => UserDiscoveryConfig { threshold, @@ -150,9 +202,7 @@ impl UserDiscovery UserDiscovery)` - The current version of the user discovery - /// * `Err(UserDiscoveryError)` - If there where errors in the store. + /// * `Err(TwonlyError)` - If there where errors in the store. /// pub async fn get_current_version(&self) -> Result> { let config = self.get_config_snapshot().await?; @@ -182,21 +232,6 @@ impl UserDiscovery)>>)` - All connections the user has discovered - /// * `Err(UserDiscoveryError)` - If there where erros in the store. - /// - #[cfg(test)] - pub async fn get_all_announced_users( - &self, - ) -> Result)>>> { - self.store.get_all_announced_users().await - } - /// /// Returns all new user discovery messages for the provided contact and his current version. /// @@ -208,16 +243,16 @@ impl UserDiscovery>)` - The new user discovery messages - /// * `Err(UserDiscoveryError)` - If there where errors in the store or if the received version is invalid. + /// * `Err(TwonlyError)` - If there where errors in the store or if the received version is invalid. /// pub async fn get_new_messages( &self, contact_id: UserID, received_version: &[u8], + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, ) -> Result>> { let mut messages = vec![]; let received_version = UserDiscoveryVersion::decode(received_version)?; - let config = self.get_config_snapshot().await?; let version = Some(UserDiscoveryVersion { announcement: config.announcement_version, @@ -225,105 +260,78 @@ impl UserDiscovery ?", + received_version.promotion, + ) + .fetch_all(&mut **t) + .await + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?; let size = promoting_messages.len(); let mut filtered: Vec> = promoting_messages .into_iter() - .filter(|x| !x.is_empty()) // filter ignored versions + .filter(|message| !message.is_empty()) .collect(); - if filtered.len() != size { - // ensure the receiver will get the later version in case the last message was filtered out filtered.push( UserDiscoveryMessage { - version: Some(UserDiscoveryVersion { - announcement: config.announcement_version, - promotion: config.promotion_version, - }), + version, ..Default::default() } .encode_to_vec(), ); } - messages.extend_from_slice(&filtered); + messages.extend(filtered); } + Ok(messages) } - /// - /// Checks if the provided user has new announcements and a request of update should be send. - /// - /// # Arguments - /// - /// * `contact_id` - The contact id of the user - /// * `version` - The current version of the user discovery from the contact - /// - /// # Returns - /// - /// * `Ok(bool)` - True if the user has new announcements - /// * `Err(UserDiscoveryError)` - If there where errors in the store or if the received version is invalid. - /// - pub async fn should_request_new_messages( - &self, - contact_id: UserID, - version: &[u8], - ) -> Result>> { - let received_version = UserDiscoveryVersion::decode(version)?; - let stored_version = match self.store.get_contact_version(contact_id).await? { - Some(buf) => UserDiscoveryVersion::decode(buf.as_slice())?, - None => UserDiscoveryVersion { - announcement: 0, - promotion: 0, - }, - }; - tracing::debug!( - received.announcement = %received_version.announcement, - received.promotion = %received_version.promotion, - stored.announcement = %stored_version.announcement, - stored.promotion = %stored_version.promotion, - "Comparing version numbers" - ); - if received_version.announcement > stored_version.announcement - || received_version.promotion > stored_version.promotion - { - Ok(Some(stored_version.encode_to_vec())) - } else { - Ok(None) - } - } - - #[cfg(test)] - pub(crate) async fn get_contact_version(&self, contact_id: UserID) -> Result>> { - self.store.get_contact_version(contact_id).await - } - /// Returns the latest version for this discovery. /// Before calling this function the application must sure that contact_id is qualified to be announced. pub async fn handle_new_messages( @@ -331,6 +339,7 @@ impl UserDiscovery, messages: Vec>, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, ) -> Result<()> { for message in messages { let Ok(message) = UserDiscoveryMessage::decode(message.as_slice()) else { @@ -347,21 +356,31 @@ impl UserDiscovery UserDiscovery, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, ) -> Result<()> { - let Some(current_promotion) = self.store.get_contact_promotion(contact_id).await? else { + let current_promotion = sqlx::query_scalar!( + r#"SELECT promotion FROM user_discovery_own_promotions + WHERE contact_id = ? ORDER BY version_id DESC LIMIT 1"#, + contact_id, + ) + .fetch_optional(&mut **t) + .await + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?; + let Some(current_promotion) = current_promotion else { // User does not participate... return Ok(()); }; @@ -409,13 +437,22 @@ impl UserDiscovery UserDiscovery, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, ) -> Result>> { tracing::debug!( "Initializing user discovery with {} total shares and with a threshold of {}", @@ -450,7 +488,7 @@ impl UserDiscovery UserDiscovery UserDiscovery Result { - Ok(serde_json::from_str(&self.store.get_config().await?)?) + Ok(serde_json::from_str(&self.read_config()?)?) + } + + fn read_config(&self) -> Result { + if !self.config_path.is_file() { + return Err(TwonlyError::UserDiscoveryNotInitialized); + } + Ok(std::fs::read_to_string(&self.config_path)?) + } + + fn write_config(&self, config: &UserDiscoveryConfig) -> Result<()> { + std::fs::write(&self.config_path, serde_json::to_string_pretty(config)?)?; + Ok(()) } /// Atomically reads the config, applies the mutation, and writes it back. @@ -488,12 +550,9 @@ impl UserDiscovery UserDiscovery, uda: UserDiscoveryAnnouncement, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, ) -> Result<()> { tracing::info!("Got a user discovery announcement from {contact_id}."); @@ -528,30 +588,28 @@ impl UserDiscovery UserDiscovery UserDiscovery UserDiscovery Err(UserDiscoveryError::ShamirsSecret(err.to_string())), + Err(err) => Err(TwonlyError::ShamirsSecret(err.to_string())), } } @@ -647,6 +713,7 @@ impl UserDiscovery, ) -> Result<()> { tracing::debug!("Received a new UDP with public_id = {}.", &udp.public_id); @@ -655,20 +722,21 @@ impl UserDiscovery UserDiscovery UserDiscovery UserDiscovery UserDiscovery Err(UserDiscoveryError::ShamirsSecret(err.to_string())), + Err(err) => Err(TwonlyError::ShamirsSecret(err.to_string())), } } + + async fn store_other_promotion( + &self, + promotion: OtherPromotion, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + ) -> Result<()> { + sqlx::query!( + r#" + INSERT INTO user_discovery_other_promotions ( + from_contact_id, promotion_id, public_id, threshold, + announcement_share, public_key_verified_timestamp + ) VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(from_contact_id, public_id) DO UPDATE SET + promotion_id = excluded.promotion_id, + threshold = excluded.threshold, + announcement_share = excluded.announcement_share, + public_key_verified_timestamp = excluded.public_key_verified_timestamp + "#, + promotion.from_contact_id, + promotion.promotion_id, + promotion.public_id, + promotion.threshold, + promotion.announcement_share, + promotion.public_key_verified_timestamp, + ) + .execute(&mut **t) + .await + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?; + Ok(()) + } + + async fn get_announced_user_by_public_id( + &self, + public_id: i64, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + ) -> Result> { + let row = sqlx::query!( + r#" + SELECT announced_user_id, announced_public_key, public_id + FROM user_discovery_announced_users + WHERE public_id = ? + "#, + public_id, + ) + .fetch_optional(&mut **t) + .await + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?; + Ok(row.map(|row| AnnouncedUser { + user_id: row.announced_user_id, + public_key: row.announced_public_key, + public_id: row.public_id, + })) + } + + async fn get_other_promotions_by_public_id( + &self, + public_id: i64, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + ) -> Result> { + let rows = sqlx::query!( + r#" + SELECT promotion_id, public_id, from_contact_id, threshold, + announcement_share, public_key_verified_timestamp + FROM user_discovery_other_promotions + WHERE public_id = ? + "#, + public_id, + ) + .fetch_all(&mut **t) + .await + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?; + rows.into_iter() + .map(|row| { + Ok(OtherPromotion { + promotion_id: u32::try_from(row.promotion_id) + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?, + public_id: row.public_id, + from_contact_id: row.from_contact_id, + threshold: u8::try_from(row.threshold) + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?, + announcement_share: row.announcement_share, + public_key_verified_timestamp: row.public_key_verified_timestamp, + }) + }) + .collect() + } + + async fn push_new_user_relation( + &self, + from_contact_id: UserID, + announced_user: AnnouncedUser, + public_key_verified_timestamp: Option, + t: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + ) -> Result<()> { + sqlx::query!( + r#" + INSERT INTO user_discovery_announced_users ( + announced_user_id, announced_public_key, public_id + ) VALUES (?, ?, ?) + ON CONFLICT DO UPDATE SET + announced_user_id = excluded.announced_user_id, + announced_public_key = excluded.announced_public_key, + public_id = excluded.public_id + "#, + announced_user.user_id, + announced_user.public_key, + announced_user.public_id, + ) + .execute(&mut **t) + .await + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?; + sqlx::query!( + r#" + INSERT INTO user_discovery_user_relations ( + announced_user_id, from_contact_id, public_key_verified_timestamp + ) VALUES (?, ?, ?) + ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET + public_key_verified_timestamp = excluded.public_key_verified_timestamp + "#, + announced_user.user_id, + from_contact_id, + public_key_verified_timestamp, + ) + .execute(&mut **t) + .await + .map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?; + Ok(()) + } } impl Default for UserDiscoveryConfig { diff --git a/rust/src/user_discovery/error.rs b/rust/src/user_discovery/error.rs deleted file mode 100644 index fd4b6767..00000000 --- a/rust/src/user_discovery/error.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -use prost::DecodeError; -use thiserror::Error; - -pub type Result = core::result::Result; - -#[derive(Error, Debug)] -pub enum UserDiscoveryError { - #[error("Store error: `{0}`")] - Store(String), - - #[error("The encrypted announcement data contains malicious data: `{0}`")] - MaliciousAnnouncementData(String), - - #[error("no shares left.")] - NoSharesLeft, - - #[error("User discovery contains no configuration.")] - NotInitialized, - - #[error("`{0}`")] - JsonError(#[from] serde_json::Error), - - #[error("`{0}`")] - IoError(#[from] std::io::Error), - - #[error("error while calculating shamirs secret shares: `{0}`")] - ShamirsSecret(String), - - #[error("tried to push a invalid version")] - PushedInvalidVersion, - - #[error("`{0}`")] - Prost(#[from] DecodeError), -} diff --git a/rust/src/user_discovery/stores/in_memory_store.rs b/rust/src/user_discovery/stores/in_memory_store.rs deleted file mode 100644 index d7fbbcc0..00000000 --- a/rust/src/user_discovery/stores/in_memory_store.rs +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -use crate::user_discovery::error::{Result, UserDiscoveryError}; -use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore}; -use crate::user_discovery::UserID; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; - -#[derive(Default)] -pub(crate) struct Storage { - config: Option, - unused_shares: Vec>, - used_shares: HashMap>, - contact_versions: HashMap>, - other_promotions: HashMap<(UserID, i64), OtherPromotion>, - announced_users: HashMap)>>, - own_promotions: Vec<(UserID, Vec)>, -} - -#[derive(Default, Clone)] -pub struct InMemoryStore { - pub(crate) storage: Arc>, -} - -impl InMemoryStore { - fn storage(&self) -> std::sync::MutexGuard<'_, Storage> { - self.storage.lock().unwrap() - } -} - -impl UserDiscoveryStore for InMemoryStore { - async fn get_config(&self) -> Result { - if let Some(storage) = self.storage().config.clone() { - return Ok(storage); - } - Err(UserDiscoveryError::NotInitialized) - } - - async fn update_config(&self, update: String) -> Result<()> { - self.storage().config = Some(update); - Ok(()) - } - - async fn set_shares(&self, shares: Vec>) -> Result<()> { - self.storage().used_shares.clear(); - self.storage().unused_shares = shares; - Ok(()) - } - - async fn get_share_for_contact(&self, contact_id: UserID) -> Result> { - let mut storage = self.storage(); - if let Some(share) = storage.used_shares.get(&contact_id) { - return Ok(share.to_vec()); - } - if let Some(new_share) = storage.unused_shares.pop() { - storage.used_shares.insert(contact_id, new_share.clone()); - return Ok(new_share); - } - Err(UserDiscoveryError::NoSharesLeft) - } - - async fn get_contact_version(&self, contact_id: UserID) -> Result>> { - Ok(self.storage().contact_versions.get(&contact_id).cloned()) - } - - async fn set_contact_version(&self, contact_id: UserID, update: Vec) -> Result<()> { - self.storage().contact_versions.insert(contact_id, update); - Ok(()) - } - - async fn push_own_promotion_and_clear_old_version( - &self, - contact_id: UserID, - version: u32, - promotion: Vec, - ) -> Result<()> { - let mut storage = self.storage(); - - if version as usize != storage.own_promotions.len() + 1 { - return Err(UserDiscoveryError::PushedInvalidVersion); - } - - for (old_contact_id, promotion) in storage.own_promotions.iter_mut() { - if *old_contact_id == contact_id { - promotion.clear(); - } - } - - storage.own_promotions.push((contact_id, promotion)); - Ok(()) - } - - async fn get_own_promotions_after_version(&self, version: u32) -> Result>> { - let storage = self.storage(); - let elements = storage.own_promotions[(version as usize)..] - .iter() - .map(|(_, promotion)| promotion.to_owned()) - .collect(); - Ok(elements) - } - - async fn get_contact_promotion(&self, contact_id: UserID) -> Result>> { - let storage = self.storage(); - let element = storage - .own_promotions - .iter() - .rev() - .find(|(c_id, _)| *c_id == contact_id); - if let Some(element) = element { - return Ok(Some(element.1.to_owned())); - } - Ok(None) - } - - async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> { - self.storage() - .other_promotions - .insert((promotion.from_contact_id, promotion.public_id), promotion); - Ok(()) - } - - async fn get_other_promotions_by_public_id( - &self, - public_id: i64, - ) -> Result> { - Ok(self - .storage() - .other_promotions - .clone() - .into_values() - .filter(|other| other.public_id == public_id) - .collect()) - } - - async fn get_announced_user_by_public_id( - &self, - public_id: i64, - ) -> Result> { - Ok(self - .storage() - .announced_users - .iter() - .find(|(u, _)| u.public_id == public_id) - .map(|u| u.0.to_owned())) - } - - async fn get_all_announced_users( - &self, - ) -> Result)>>> { - Ok(self.storage().announced_users.clone()) - } - - async fn push_new_user_relation( - &self, - from_contact_id: UserID, - announced_user: AnnouncedUser, - public_key_verified_timestamp: Option, - ) -> Result<()> { - let mut storage = self.storage(); - let entry = storage - .announced_users - .entry(announced_user.clone()) - .or_default(); - if announced_user.user_id != from_contact_id { - if let Some(found) = entry.iter_mut().find(|x| x.0 == from_contact_id) { - found.1 = public_key_verified_timestamp; - } else { - entry.push((from_contact_id, public_key_verified_timestamp)); - } - } - Ok(()) - } -} diff --git a/rust/src/user_discovery/stores/mod.rs b/rust/src/user_discovery/stores/mod.rs deleted file mode 100644 index cf55c239..00000000 --- a/rust/src/user_discovery/stores/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -#[cfg(test)] -mod in_memory_store; -#[cfg(test)] -pub(super) use in_memory_store::InMemoryStore; - -mod native; -pub(crate) use native::{NativeUserDiscoveryStore, NativeUserDiscoveryUtils}; diff --git a/rust/src/user_discovery/stores/native.rs b/rust/src/user_discovery/stores/native.rs deleted file mode 100644 index 05409338..00000000 --- a/rust/src/user_discovery/stores/native.rs +++ /dev/null @@ -1,565 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -use crate::database::{app::AppDatabase, signal::Database}; -use crate::keys::KeyManager; -use crate::user_discovery::error::{Result, UserDiscoveryError}; -use crate::user_discovery::traits::{ - AnnouncedUser, OtherPromotion, UserDiscoveryStore, UserDiscoveryUtils, -}; -use crate::user_discovery::UserID; -use libsignal_protocol::{IdentityKey, IdentityKeyPair}; -use rand::SeedableRng; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::sync::{Mutex, RwLock}; - -fn store_error(error: impl std::fmt::Display) -> UserDiscoveryError { - UserDiscoveryError::Store(error.to_string()) -} - -#[derive(Clone)] -pub(crate) struct NativeUserDiscoveryStore { - app_db: Arc>>, - config_path: PathBuf, -} - -impl NativeUserDiscoveryStore { - pub(crate) fn new(app_db: Arc>>, data_dir: &str) -> Self { - Self { - app_db, - config_path: PathBuf::from(data_dir).join("user_discovery_config.json"), - } - } - - async fn database(&self) -> Arc { - self.app_db.read().await.clone() - } -} - -impl UserDiscoveryStore for NativeUserDiscoveryStore { - async fn get_config(&self) -> Result { - if !self.config_path.is_file() { - return Err(UserDiscoveryError::NotInitialized); - } - Ok(std::fs::read_to_string(&self.config_path)?) - } - - async fn update_config(&self, update: String) -> Result<()> { - std::fs::write(&self.config_path, update)?; - Ok(()) - } - - async fn set_shares(&self, shares: Vec>) -> Result<()> { - let db = self.database().await; - let mut tx = db.pool.begin().await.map_err(store_error)?; - sqlx::query!(r#"DELETE FROM user_discovery_shares"#) - .execute(&mut *tx) - .await - .map_err(store_error)?; - for share in shares { - sqlx::query!( - r#"INSERT INTO user_discovery_shares (share) VALUES (?)"#, - share - ) - .execute(&mut *tx) - .await - .map_err(store_error)?; - } - tx.commit().await.map_err(store_error)?; - db.notify_committed(["user_discovery_shares"]); - Ok(()) - } - - async fn get_share_for_contact(&self, contact_id: UserID) -> Result> { - let db = self.database().await; - let mut tx = db.pool.begin().await.map_err(store_error)?; - if let Some(share) = sqlx::query_scalar!( - r#" - SELECT share - FROM user_discovery_shares - WHERE contact_id = ? - LIMIT 1 - "#, - contact_id, - ) - .fetch_optional(&mut *tx) - .await - .map_err(store_error)? - { - tx.commit().await.map_err(store_error)?; - return Ok(share); - } - let available = sqlx::query!( - r#" - SELECT share_id, share - FROM user_discovery_shares - WHERE contact_id IS NULL - LIMIT 1 - "#, - ) - .fetch_optional(&mut *tx) - .await - .map_err(store_error)?; - let Some(row) = available else { - tx.rollback().await.map_err(store_error)?; - return Err(UserDiscoveryError::NoSharesLeft); - }; - let share_id = row.share_id; - let share = row.share; - sqlx::query!( - r#" - UPDATE user_discovery_shares - SET contact_id = ? - WHERE share_id = ? - "#, - contact_id, - share_id, - ) - .execute(&mut *tx) - .await - .map_err(store_error)?; - tx.commit().await.map_err(store_error)?; - db.notify_committed(["user_discovery_shares"]); - Ok(share) - } - - async fn push_own_promotion_and_clear_old_version( - &self, - contact_id: UserID, - _version: u32, - promotion: Vec, - ) -> Result<()> { - let db = self.database().await; - let mut tx = db.pool.begin().await.map_err(store_error)?; - sqlx::query!( - r#" - UPDATE user_discovery_own_promotions - SET promotion = X'' - WHERE contact_id = ? - "#, - contact_id, - ) - .execute(&mut *tx) - .await - .map_err(store_error)?; - sqlx::query!( - r#" - INSERT INTO user_discovery_own_promotions ( - contact_id, - promotion - ) VALUES (?, ?) - "#, - contact_id, - promotion, - ) - .execute(&mut *tx) - .await - .map_err(store_error)?; - tx.commit().await.map_err(store_error)?; - db.notify_committed(["user_discovery_own_promotions"]); - Ok(()) - } - - async fn get_own_promotions_after_version(&self, version: u32) -> Result>> { - let db = self.database().await; - sqlx::query_scalar!( - r#" - SELECT promotion - FROM user_discovery_own_promotions - WHERE version_id > ? - "#, - version, - ) - .fetch_all(&db.pool) - .await - .map_err(store_error) - } - - async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> { - let db = self.database().await; - sqlx::query!( - r#" - INSERT INTO user_discovery_other_promotions ( - from_contact_id, - promotion_id, - public_id, - threshold, - announcement_share, - public_key_verified_timestamp - ) VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(from_contact_id, public_id) DO UPDATE SET - promotion_id = excluded.promotion_id, - threshold = excluded.threshold, - announcement_share = excluded.announcement_share, - public_key_verified_timestamp = excluded.public_key_verified_timestamp - "#, - promotion.from_contact_id, - promotion.promotion_id, - promotion.public_id, - promotion.threshold, - promotion.announcement_share, - promotion.public_key_verified_timestamp, - ) - .execute(&db.pool) - .await - .map_err(store_error)?; - db.notify_committed(["user_discovery_other_promotions"]); - Ok(()) - } - - async fn get_other_promotions_by_public_id( - &self, - public_id: i64, - ) -> Result> { - let db = self.database().await; - let rows = sqlx::query!( - r#" - SELECT - promotion_id, - public_id, - from_contact_id, - threshold, - announcement_share, - public_key_verified_timestamp - FROM user_discovery_other_promotions - WHERE public_id = ? - "#, - public_id, - ) - .fetch_all(&db.pool) - .await - .map_err(store_error)?; - rows.into_iter() - .map(|row| { - Ok(OtherPromotion { - promotion_id: u32::try_from(row.promotion_id).map_err(store_error)?, - public_id: row.public_id, - from_contact_id: row.from_contact_id, - threshold: u8::try_from(row.threshold).map_err(store_error)?, - announcement_share: row.announcement_share, - public_key_verified_timestamp: row.public_key_verified_timestamp, - }) - }) - .collect() - } - - async fn get_announced_user_by_public_id( - &self, - public_id: i64, - ) -> Result> { - let db = self.database().await; - let row = sqlx::query!( - r#" - SELECT - announced_user_id, - announced_public_key, - public_id - FROM user_discovery_announced_users - WHERE public_id = ? - "#, - public_id, - ) - .fetch_optional(&db.pool) - .await - .map_err(store_error)?; - row.map(|row| { - Ok(AnnouncedUser { - user_id: row.announced_user_id, - public_key: row.announced_public_key, - public_id: row.public_id, - }) - }) - .transpose() - } - - async fn push_new_user_relation( - &self, - from_contact_id: UserID, - announced_user: AnnouncedUser, - public_key_verified_timestamp: Option, - ) -> Result<()> { - let db = self.database().await; - let mut tx = db.pool.begin().await.map_err(store_error)?; - sqlx::query!( - r#" - INSERT INTO user_discovery_announced_users ( - announced_user_id, - announced_public_key, - public_id - ) VALUES (?, ?, ?) - ON CONFLICT DO UPDATE SET - announced_user_id = excluded.announced_user_id, - announced_public_key = excluded.announced_public_key, - public_id = excluded.public_id - "#, - announced_user.user_id, - announced_user.public_key, - announced_user.public_id, - ) - .execute(&mut *tx) - .await - .map_err(store_error)?; - sqlx::query!( - r#" - INSERT INTO user_discovery_user_relations ( - announced_user_id, - from_contact_id, - public_key_verified_timestamp - ) VALUES (?, ?, ?) - ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET - public_key_verified_timestamp = excluded.public_key_verified_timestamp - "#, - announced_user.user_id, - from_contact_id, - public_key_verified_timestamp, - ) - .execute(&mut *tx) - .await - .map_err(store_error)?; - tx.commit().await.map_err(store_error)?; - db.notify_committed([ - "user_discovery_announced_users", - "user_discovery_user_relations", - ]); - Ok(()) - } - - #[cfg(test)] - async fn get_all_announced_users( - &self, - ) -> Result)>>> { - Err(UserDiscoveryError::Store("not used by native store".into())) - } - - async fn get_contact_promotion(&self, contact_id: UserID) -> Result>> { - let db = self.database().await; - sqlx::query_scalar!( - r#" - SELECT promotion - FROM user_discovery_own_promotions - WHERE contact_id = ? - ORDER BY version_id DESC - LIMIT 1 - "#, - contact_id, - ) - .fetch_optional(&db.pool) - .await - .map_err(store_error) - } - - async fn get_contact_version(&self, contact_id: UserID) -> Result>> { - let db = self.database().await; - sqlx::query_scalar!( - r#" - SELECT user_discovery_version - FROM contacts - WHERE user_id = ? - "#, - contact_id, - ) - .fetch_optional(&db.pool) - .await - .map_err(store_error) - .map(Option::flatten) - } - - async fn set_contact_version(&self, contact_id: UserID, update: Vec) -> Result<()> { - let db = self.database().await; - sqlx::query!( - r#" - UPDATE contacts - SET user_discovery_version = ? - WHERE user_id = ? - "#, - update, - contact_id, - ) - .execute(&db.pool) - .await - .map_err(store_error)?; - db.notify_committed(["contacts"]); - Ok(()) - } -} - -pub(crate) struct NativeUserDiscoveryUtils { - key_manager: Arc>, - rust_db: Arc>>, -} - -impl NativeUserDiscoveryUtils { - pub(crate) fn new( - key_manager: Arc>, - rust_db: Arc>>, - ) -> Self { - Self { - key_manager, - rust_db, - } - } -} - -impl UserDiscoveryUtils for NativeUserDiscoveryUtils { - async fn sign_data(&self, input_data: &[u8]) -> Result> { - let key_manager = self.key_manager.lock().await; - let identity = key_manager - .signal_identity - .as_ref() - .ok_or_else(|| UserDiscoveryError::Store("no Signal identity found".into()))?; - let key_pair = IdentityKeyPair::try_from(identity.identity_key_pair_structure.as_slice()) - .map_err(store_error)?; - let mut csprng = rand::rngs::StdRng::from_os_rng(); - key_pair - .private_key() - .calculate_signature_for_multipart_message(&[input_data], &mut csprng) - .map(|signature| signature.to_vec()) - .map_err(store_error) - } - - async fn verify_signature( - &self, - input_data: &[u8], - pubkey: &[u8], - signature: &[u8], - ) -> Result { - let identity = match IdentityKey::decode(pubkey) { - Ok(identity) => identity, - Err(_) => return Ok(false), - }; - Ok(identity - .public_key() - .verify_signature(input_data, signature)) - } - - async fn verify_stored_pubkey(&self, from_contact_id: UserID, pubkey: &[u8]) -> Result { - let db = self.rust_db.read().await.clone(); - let stored = sqlx::query_scalar!( - r#" - SELECT identity_key - FROM signal_identities - WHERE name = ? - "#, - from_contact_id.to_string(), - ) - .fetch_optional(&db.pool) - .await - .map_err(store_error)?; - Ok(stored.as_deref() == Some(pubkey)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::keys::SignalIdentityKey; - use std::collections::HashMap; - - #[tokio::test] - async fn native_store_and_signal_utils_use_rust_owned_state() { - let temp = tempfile::tempdir().unwrap(); - let app_path = temp.path().join("app.sqlite"); - let app_db = AppDatabase::new(app_path.to_str().unwrap(), None, false) - .await - .unwrap(); - app_db.run_migrations().await.unwrap(); - sqlx::query!( - r#" - INSERT INTO contacts (user_id, username) - VALUES (1, 'one'), (2, 'two') - "# - ) - .execute(&app_db.pool) - .await - .unwrap(); - let app_handle = Arc::new(RwLock::new(Arc::new(app_db))); - let store = NativeUserDiscoveryStore::new(app_handle, temp.path().to_str().unwrap()); - - store.update_config("config".into()).await.unwrap(); - assert_eq!(store.get_config().await.unwrap(), "config"); - store.set_shares(vec![vec![1], vec![2]]).await.unwrap(); - let assigned = store.get_share_for_contact(1).await.unwrap(); - assert_eq!(store.get_share_for_contact(1).await.unwrap(), assigned); - store.set_contact_version(1, vec![7]).await.unwrap(); - assert_eq!(store.get_contact_version(1).await.unwrap(), Some(vec![7])); - store - .push_own_promotion_and_clear_old_version(1, 1, vec![8]) - .await - .unwrap(); - assert_eq!(store.get_contact_promotion(1).await.unwrap(), Some(vec![8])); - - let promotion = OtherPromotion { - promotion_id: 3, - public_id: 4, - from_contact_id: 1, - threshold: 2, - announcement_share: vec![5], - public_key_verified_timestamp: Some(6), - }; - store.store_other_promotion(promotion).await.unwrap(); - assert_eq!( - store - .get_other_promotions_by_public_id(4) - .await - .unwrap() - .len(), - 1 - ); - let announced = AnnouncedUser { - user_id: 2, - public_key: vec![9], - public_id: 10, - }; - store - .push_new_user_relation(1, announced, Some(11)) - .await - .unwrap(); - assert_eq!( - store - .get_announced_user_by_public_id(10) - .await - .unwrap() - .unwrap() - .user_id, - 2 - ); - - let rust_path = temp.path().join("rust.sqlite"); - let rust_path_string = rust_path.display().to_string(); - let rust_db = Database::new(&rust_path_string, None, false).await.unwrap(); - rust_db.run_migrations().await.unwrap(); - let rust_handle = Arc::new(RwLock::new(Arc::new(rust_db))); - - let mut rng = rand::rng(); - let identity = IdentityKeyPair::generate(&mut rng); - let public_key = identity.identity_key().serialize().to_vec(); - sqlx::query!( - r#" - INSERT INTO signal_identities (name, identity_key, timestamp) - VALUES ('1', ?, 0) - "#, - &public_key, - ) - .execute(&rust_handle.read().await.pool) - .await - .unwrap(); - let mut key_manager = KeyManager::generate().unwrap(); - key_manager.signal_identity = Some(SignalIdentityKey { - identity_key_pair_structure: identity.serialize().to_vec(), - registration_id: 1, - pre_key_store: HashMap::new(), - }); - let utils = NativeUserDiscoveryUtils::new(Arc::new(Mutex::new(key_manager)), rust_handle); - let message = b"native discovery"; - let signature = utils.sign_data(message).await.unwrap(); - assert!(utils - .verify_signature(message, &public_key, &signature) - .await - .unwrap()); - assert!(utils.verify_stored_pubkey(1, &public_key).await.unwrap()); - assert!(!utils.verify_stored_pubkey(2, &public_key).await.unwrap()); - } -} diff --git a/rust/src/user_discovery/tests.rs b/rust/src/user_discovery/tests.rs deleted file mode 100644 index 43dd0437..00000000 --- a/rust/src/user_discovery/tests.rs +++ /dev/null @@ -1,533 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -use crate::user_discovery::stores::InMemoryStore; -use crate::user_discovery::traits::tests::TestingUtils; -use crate::user_discovery::{UserDiscovery, UserDiscoveryStore, UserDiscoveryVersion, UserID}; -use prost::Message; -use rand::seq::SliceRandom; -use std::collections::{HashMap, HashSet}; -use std::vec; - -struct TestNetwork { - ids_by_name: HashMap<&'static str, usize>, - names: Vec<&'static str>, - friends: Vec>, - message_flows: Vec<(usize, usize)>, - announced_users_expected: Vec)>>, - uds: Vec>, -} - -impl TestNetwork { - fn new() -> Self { - Self { - ids_by_name: HashMap::new(), - names: vec![], - friends: vec![], - message_flows: vec![], - announced_users_expected: vec![], - uds: vec![], - } - } - - async fn add_user(&mut self, name: &'static str, threshold: u8) { - let id = self.names.len(); - self.ids_by_name.insert(name, id); - self.names.push(name); - self.friends.push(vec![]); - self.announced_users_expected.push(vec![]); - - let store = S::default(); - self.uds.push(get_ud(id, threshold, store).await); - } - - fn set_friends(&mut self, user: &str, friends: &[&str]) { - let id = self.ids_by_name[user]; - let f_ids: Vec = friends.iter().map(|f| self.ids_by_name[*f]).collect(); - self.friends[id] = f_ids; - } - - fn add_message_flow(&mut self, user: &str, count: usize) { - let id = self.ids_by_name[user]; - self.message_flows.push((id, count)); - } - - fn expect_announced_user(&mut self, user: &str, contact: &str, friends_of_contact: &[&str]) { - let user_id = self.ids_by_name[user]; - let contact_id = self.ids_by_name[contact]; - let f_ids: Vec = friends_of_contact - .iter() - .map(|f| self.ids_by_name[*f]) - .collect(); - self.announced_users_expected[user_id].push((contact_id, f_ids)); - } -} - -async fn get_with_five_users() -> TestNetwork { - let mut network = TestNetwork::new(); - - network.add_user("ALICE", 2).await; - network.add_user("BOB", 2).await; - network.add_user("CHARLIE", 2).await; - network.add_user("DAVID", 2).await; - network.add_user("FRANK", 2).await; - - network.set_friends("ALICE", &["BOB", "CHARLIE"]); - network.set_friends("BOB", &["ALICE", "CHARLIE", "DAVID"]); - network.set_friends("CHARLIE", &["ALICE", "BOB", "DAVID", "FRANK"]); - network.set_friends("DAVID", &["BOB", "CHARLIE"]); - network.set_friends("FRANK", &["CHARLIE"]); - - network.add_message_flow("ALICE", 1); // ALICE: own announcement sending to BOB and CHARLIE - network.add_message_flow("BOB", 2); // BOB: own announcement + promotion for ALICE - network.add_message_flow("BOB", 0); // BOBs version should not have any new messages for his friends - network.add_message_flow("ALICE", 1); // ALICE: promotion for BOB - network.add_message_flow("CHARLIE", 3); // CHARLIE: own announcement + promotion for ALICE, BOB - network.add_message_flow("DAVID", 3); // DAVID: own announcement + promotion for BOB, CHARLIE - network.add_message_flow("BOB", 2); // BOB: promotion for CHARLIE, DAVID - network.add_message_flow("CHARLIE", 1); // CHARLIE: promotion for DAVID - network.add_message_flow("FRANK", 2); // FRANK: own announcement + promotion for CHARLIE - network.add_message_flow("CHARLIE", 1); // CHARLIE: promotion for FRANK - network.add_message_flow("ALICE", 1); // ALICE: promotion for CHARLIE - - // ALICE should now know that BOB and CHARLIE, BOB and DAVID and CHARLIE and DAVID are friends. - // Alice should also have one protected share from Frank. - network.expect_announced_user("ALICE", "BOB", &["CHARLIE"]); // ALICE knows Bob and that CHARLIE is connected with BOB - network.expect_announced_user("ALICE", "CHARLIE", &["BOB"]); // ALICE knows CHARLIE and that BOB is connected with CHARLIE - network.expect_announced_user("ALICE", "DAVID", &["BOB", "CHARLIE"]); // ALICE knows DAVID and that BOB and CHARLIE are connected with DAVID - - network.expect_announced_user("BOB", "ALICE", &["CHARLIE"]); - network.expect_announced_user("BOB", "CHARLIE", &["ALICE", "DAVID"]); - network.expect_announced_user("BOB", "DAVID", &["CHARLIE"]); - - network.expect_announced_user("CHARLIE", "ALICE", &["BOB"]); - network.expect_announced_user("CHARLIE", "BOB", &["ALICE", "DAVID"]); - network.expect_announced_user("CHARLIE", "DAVID", &["BOB"]); - network.expect_announced_user("CHARLIE", "FRANK", &[]); - - network.expect_announced_user("DAVID", "ALICE", &["BOB", "CHARLIE"]); - network.expect_announced_user("DAVID", "BOB", &["CHARLIE"]); - network.expect_announced_user("DAVID", "CHARLIE", &["BOB"]); - - network.expect_announced_user("FRANK", "CHARLIE", &[]); - - network -} - -#[tokio::test] -async fn test_user_discovery_decreased_threshold_in_memory_store() { - let _ = pretty_env_logger::try_init(); - - let mut network = TestNetwork::::new(); - - // Start ALICE with a more strict threshold of 3. - // David only has 2 paths to Alice (via Bob and Charlie). Since 2 < 3, David cannot discover Alice. - network.add_user("ALICE", 3).await; - network.add_user("BOB", 2).await; - network.add_user("CHARLIE", 2).await; - network.add_user("DAVID", 2).await; - network.add_user("FRANK", 2).await; - - // Same topology as the initial 5 users - network.set_friends("ALICE", &["BOB", "CHARLIE"]); - network.set_friends("BOB", &["ALICE", "CHARLIE", "DAVID"]); - network.set_friends("CHARLIE", &["ALICE", "BOB", "DAVID", "FRANK"]); - network.set_friends("DAVID", &["BOB", "CHARLIE"]); - network.set_friends("FRANK", &["CHARLIE"]); - - let david_idx = network.ids_by_name["DAVID"]; - let alice_idx = network.ids_by_name["ALICE"]; - let charlie_idx = network.ids_by_name["CHARLIE"]; - - // Phase 1: Exchange with ALICE threshold = 3 - step0_exchange_random::(&network).await; - step1_verify_no_new_messages::(&network).await; - - let version = network.uds[david_idx] - .get_contact_version(charlie_idx as UserID) - .await - .unwrap() - .unwrap(); - assert_eq!(version, get_version_bytes(1, 4)); - - // DAVID should NOT know ALICE yet because ALICE's threshold is 3, but David only has 2 shares. - let david_knows = network.uds[david_idx] - .get_all_announced_users() - .await - .unwrap(); - - let knows_alice = david_knows - .iter() - .any(|(u, _)| u.user_id == alice_idx as UserID); - - assert!(!knows_alice, "David should not know Alice yet because Alice's threshold is 3 and David only receives 2 shares"); - - // Phase 2: Update ALICE's threshold to 2 - network.uds[alice_idx] - .initialize_or_update(2, alice_idx as UserID, vec![alice_idx as u8; 32], true) - .await - .unwrap(); - - let version = network.uds[alice_idx].get_current_version().await.unwrap(); - assert_eq!(version, get_version_bytes(2, 2)); - - // ALICE's new announcement with threshold 2 should propagate further. - step0_exchange_random::(&network).await; - step1_verify_no_new_messages::(&network).await; - - let version = network.uds[david_idx] - .get_contact_version(charlie_idx as UserID) - .await - .unwrap() - .unwrap(); - assert_eq!(version, get_version_bytes(1, 5)); - - let version = network.uds[alice_idx].get_current_version().await.unwrap(); - assert_eq!(version, get_version_bytes(2, 2)); - - // Now DAVID SHOULD know ALICE - let david_knows = network.uds[david_idx] - .get_all_announced_users() - .await - .unwrap(); - let knows_alice_now = david_knows - .iter() - .any(|(u, _)| u.user_id == alice_idx as UserID); - assert!( - knows_alice_now, - "David should know Alice now because her threshold was updated to 2" - ); -} - -#[tokio::test] -async fn test_user_discovery_increased_threshold_in_memory_store() { - let _ = pretty_env_logger::try_init(); - - let mut network = TestNetwork::::new(); - - // Start ALICE with a more strict threshold of 3. - // David only has 2 paths to Alice (via Bob and Charlie). Since 2 < 3, David cannot discover Alice. - network.add_user("ALICE", 2).await; - network.add_user("BOB", 2).await; - network.add_user("CHARLIE", 2).await; - network.add_user("DAVID", 2).await; - network.add_user("FRANK", 2).await; - - // Same topology as the initial 5 users - network.set_friends("ALICE", &["BOB", "CHARLIE"]); - network.set_friends("BOB", &["ALICE", "CHARLIE", "DAVID"]); - // CHARLIE IS NOT YET A FRIEND OF DAVID -> DAVID SHOULD NOT BE ABLE TO BE DECODE ALICE - network.set_friends("CHARLIE", &["ALICE", "BOB", "FRANK"]); - network.set_friends("DAVID", &["BOB", "CHARLIE"]); - network.set_friends("FRANK", &["CHARLIE"]); - - let david_idx = network.ids_by_name["DAVID"]; - let alice_idx = network.ids_by_name["ALICE"]; - - // Phase 1: Exchange with ALICE threshold = 2 - step0_exchange_random::(&network).await; - step1_verify_no_new_messages::(&network).await; - - // DAVID should NOT know ALICE yet because ALICE's threshold is 2, and David has only 1 shares. - { - let david_knows = network.uds[david_idx] - .get_all_announced_users() - .await - .unwrap(); - - let knows_alice = david_knows - .iter() - .any(|(u, _)| u.user_id == alice_idx as UserID); - - assert!(!knows_alice, "David should not know Alice yet because Alice's threshold is 3 and David only receives 2 shares"); - } - - // Phase 2: Update ALICE's threshold to 3 - network.uds[alice_idx] - .initialize_or_update(3, alice_idx as UserID, vec![alice_idx as u8; 32], true) - .await - .unwrap(); - - // ALICE's new announcement with threshold 3 should propagate further. - // This SHOULD REPLACE THE OLD VERSION... - step0_exchange_random::(&network).await; - step1_verify_no_new_messages::(&network).await; - - // Now Charlie is a friend of David, so he should exchange ALICE with him - - network.set_friends("CHARLIE", &["ALICE", "BOB", "DAVID", "FRANK"]); - - // ALICE's new announcement with threshold 3 should propagate further. - // This SHOULD REPLACE THE OLD VERSION... - step0_exchange_random::(&network).await; - step1_verify_no_new_messages::(&network).await; - - // DAVID should still NOT know ALICE yet because ALICE's new threshold is 3, and David has only 2 shares. - { - let david_knows = network.uds[david_idx] - .get_all_announced_users() - .await - .unwrap(); - - let knows_alice = david_knows - .iter() - .any(|(u, _)| u.user_id == alice_idx as UserID); - - assert!(!knows_alice, "David should not know Alice yet because Alice's threshold is 3 and David only receives 2 shares"); - } -} - -#[tokio::test] -async fn test_user_discovery_in_memory_store() { - let _ = pretty_env_logger::try_init(); - - let users = get_with_five_users().await; - step0_exchange_in_order::(&users).await; - step1_verify_no_new_messages::(&users).await; - step2_verify_announced_users_expected::(&users).await; - - let alice_idx = users.ids_by_name["ALICE"]; - let bob_idx = users.ids_by_name["BOB"]; - let david_idx = users.ids_by_name["DAVID"]; - - users.uds[bob_idx] - .update_verification_state_for_user(alice_idx as UserID, Some(10)) - .await - .unwrap(); - - step0_exchange_random::(&users).await; - step1_verify_no_new_messages::(&users).await; - - { - let david_knows = users.uds[david_idx] - .get_all_announced_users() - .await - .unwrap(); - - let knows_alice = david_knows - .iter() - .find(|(u, _)| u.user_id == alice_idx as UserID); - - assert!(knows_alice.is_some(), "David should know Alice"); - - let bob_has_verified = knows_alice - .unwrap() - .1 - .iter() - .find(|(user_id, _)| *user_id == bob_idx as UserID) - .unwrap(); - - assert_eq!(bob_has_verified.1, Some(10)); - } -} - -#[tokio::test] -async fn test_user_discovery_random_order_in_memory_store() { - let _ = pretty_env_logger::try_init(); - - let users = get_with_five_users().await; - step0_exchange_random::(&users).await; - step1_verify_no_new_messages::(&users).await; - step2_verify_announced_users_expected::(&users).await; -} - -async fn step0_exchange_in_order(users: &TestNetwork) { - for (i, (from, count)) in users.message_flows.iter().enumerate() { - tracing::debug!("MESSAGE FLOW: {i}"); - to_all_friends(*from, *count, &users).await; - } -} - -async fn step0_exchange_random(users: &TestNetwork) { - let mut user_ids: Vec = (0..users.names.len()).collect(); - - for _ in 0..100 { - user_ids.shuffle(&mut rand::rng()); - - for from in user_ids.clone() { - for friend in &users.friends[from] { - request_and_handle_messages( - (from, &users.uds[from]), - (*friend, &users.uds[*friend]), - None, - ) - .await; - } - } - } -} - -async fn step1_verify_no_new_messages( - users: &TestNetwork, -) { - tracing::debug!("Now all users should have the newest version."); - - for from in 0..users.names.len() { - for to in &users.friends[from] { - tracing::debug!( - "Does {} has open messages for {}?", - &users.names[from], - &users.names[*to] - ); - assert_new_messages((from, &users.uds[from]), (*to, &users.uds[*to]), false).await; - } - } -} - -async fn step2_verify_announced_users_expected( - users: &TestNetwork, -) { - tracing::debug!("Test if all exchanges where successful."); - - for (user, announcements) in users.announced_users_expected.iter().enumerate() { - let announced_users2 = users.uds[user].get_all_announced_users().await.unwrap(); - let mut announced_users = HashMap::new(); - for a in announced_users2 { - announced_users.insert(a.0.user_id, a.1.iter().map(|x| x.0).collect::>()); - } - tracing::debug!("{} knows now: {}", users.names[user], announced_users.len()); - assert_eq!(announced_users.len(), announcements.len()); - for (contact_id, announced_users_expected) in announcements { - let announced_users = announced_users.get(&(*contact_id as i64)).unwrap(); - tracing::debug!( - "{} knows now that {} has the following friends: {}", - users.names[user], - users.names[*contact_id], - announced_users - .iter() - .map(|x| users.names[*x as usize]) - .collect::>() - .join(", ") - ); - let announced_users: HashSet = announced_users.iter().cloned().collect(); - let announced_users_expected: HashSet = announced_users_expected - .iter() - .cloned() - .map(|x| x as i64) - .collect(); - assert_eq!(announced_users, announced_users_expected); - } - } -} - -async fn get_ud( - user_id: usize, - threshold: u8, - store: S, -) -> UserDiscovery { - let ud = UserDiscovery::new(store.to_owned(), TestingUtils::default()).unwrap(); - - ud.initialize_or_update(threshold, user_id as UserID, vec![user_id as u8; 32], true) - .await - .unwrap(); - - let version = ud.get_current_version().await.unwrap(); - - assert_eq!(version, get_version_bytes(1, 0)); - ud -} - -async fn assert_new_messages( - from: (usize, &UserDiscovery), - to: (usize, &UserDiscovery), - has_new_messages: bool, -) { - // From sends a message with his current version to To - let to_received_version = &from.1.get_current_version().await.unwrap(); - assert_eq!( - to.1.should_request_new_messages(from.0 as UserID, to_received_version) - .await - .unwrap() - .is_some(), - has_new_messages - ); -} - -async fn request_and_handle_messages( - from: (usize, &UserDiscovery), - to: (usize, &UserDiscovery), - messages_count: Option, -) { - // From sends a message with his current version to To - - if messages_count.is_some() { - let to_received_version = &from.1.get_current_version().await.unwrap(); - assert_eq!( - to.1.should_request_new_messages(from.0 as UserID, to_received_version) - .await - .unwrap() - .is_some(), - true - ); - } - - // As To has a older version stored he sends a request to From: Give me all messages since version. - let from_request_version_from_to = - to.1.get_contact_version(from.0 as UserID) - .await - .unwrap() - .unwrap_or(get_version_bytes(0, 0)); - - let new_messages = from - .1 - .get_new_messages(to.0 as UserID, &from_request_version_from_to) - .await - .unwrap(); - - if let Some(messages_count) = messages_count { - assert!(new_messages.len() <= messages_count); - } - - to.1.handle_new_messages(from.0 as UserID, None, new_messages) - .await - .unwrap(); - - if messages_count.is_some() { - assert_eq!( - to.1.should_request_new_messages( - from.0 as UserID, - &from.1.get_current_version().await.unwrap() - ) - .await - .unwrap() - .is_some(), - false - ); - } -} - -async fn to_all_friends( - from: usize, - message_count: usize, - users: &TestNetwork, -) { - for friend in &users.friends[from] { - tracing::debug!("From {} to {}", users.names[from], users.names[*friend]); - - if message_count == 0 { - assert_new_messages( - (from, &users.uds[from]), - (*friend, &users.uds[*friend]), - false, - ) - .await; - } else { - request_and_handle_messages( - (from, &users.uds[from]), - (*friend, &users.uds[*friend]), - Some(message_count), - ) - .await; - } - } -} - -fn get_version_bytes(announcement: u32, promotion: u32) -> Vec { - UserDiscoveryVersion { - announcement, - promotion, - } - .encode_to_vec() -} diff --git a/rust/src/user_discovery/traits.rs b/rust/src/user_discovery/traits.rs deleted file mode 100644 index 5ca1425d..00000000 --- a/rust/src/user_discovery/traits.rs +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -#[cfg(test)] -use std::collections::HashMap; - -use crate::user_discovery::error::Result; -use crate::user_discovery::UserID; -use std::future::Future; - -/// Type alias used in `UserDiscoveryStore::get_all_announced_users`. -#[cfg(test)] -pub type AnnouncedUserMap = HashMap)>>; - -#[derive(Clone, sqlx::FromRow)] -pub struct OtherPromotion { - pub promotion_id: u32, - pub public_id: i64, - pub from_contact_id: UserID, - pub threshold: u8, - pub announcement_share: Vec, - pub public_key_verified_timestamp: Option, -} - -#[derive(Clone, Hash, PartialEq, Eq, Debug)] -pub struct AnnouncedUser { - pub user_id: UserID, - pub public_key: Vec, - pub public_id: i64, -} - -pub trait UserDiscoveryStore { - fn get_config(&self) -> impl Future> + Send; - fn update_config(&self, update: String) -> impl Future> + Send; - fn set_shares(&self, shares: Vec>) -> impl Future> + Send; - - fn get_share_for_contact( - &self, - contact_id: UserID, - ) -> impl Future>> + Send; - - fn push_own_promotion_and_clear_old_version( - &self, - contact_id: UserID, - version: u32, - promotion: Vec, - ) -> impl Future> + Send; - - fn get_own_promotions_after_version( - &self, - version: u32, - ) -> impl Future>>> + Send; - - fn store_other_promotion( - &self, - promotion: OtherPromotion, - ) -> impl Future> + Send; - - fn get_other_promotions_by_public_id( - &self, - public_id: i64, - ) -> impl Future>> + Send; - - fn get_announced_user_by_public_id( - &self, - public_id: i64, - ) -> impl Future>> + Send; - - fn push_new_user_relation( - &self, - from_contact_id: UserID, - announced_user: AnnouncedUser, - public_key_verified_timestamp: Option, - ) -> impl Future> + Send; - - #[cfg(test)] - fn get_all_announced_users(&self) -> impl Future> + Send; - - fn get_contact_promotion( - &self, - contact_id: UserID, - ) -> impl Future>>> + Send; - - fn get_contact_version( - &self, - contact_id: UserID, - ) -> impl Future>>> + Send; - - fn set_contact_version( - &self, - contact_id: UserID, - update: Vec, - ) -> impl Future> + Send; -} - -pub trait UserDiscoveryUtils { - fn sign_data(&self, input_data: &[u8]) -> impl Future>> + Send; - fn verify_signature( - &self, - input_data: &[u8], - pubkey: &[u8], - signature: &[u8], - ) -> impl Future> + Send; - /// In case the the user does not exists yet return false. - /// If this happens this should trigger an error, as this functions is only when a message was received from this user... - /// This is used to verify that the share of the promotions contains the same public key and the user is not secretly announcing a different one - fn verify_stored_pubkey( - &self, - from_contact_id: UserID, - pubkey: &[u8], - ) -> impl Future> + Send; -} - -#[cfg(test)] -pub(crate) mod tests { - use crate::user_discovery::traits::UserDiscoveryUtils; - - #[derive(Default)] - pub(crate) struct TestingUtils {} - impl UserDiscoveryUtils for TestingUtils { - async fn sign_data( - &self, - _input_data: &[u8], - ) -> crate::user_discovery::error::Result> { - Ok(vec![0; 64]) - } - - async fn verify_signature( - &self, - _data: &[u8], - _pubkey: &[u8], - _signature: &[u8], - ) -> crate::user_discovery::error::Result { - Ok(true) - } - - async fn verify_stored_pubkey( - &self, - _from_contact_id: crate::user_discovery::UserID, - _pubkey: &[u8], - ) -> crate::user_discovery::error::Result { - Ok(true) - } - } -} diff --git a/rust/src/utils.rs b/rust/src/utils.rs index 45c05d84..96ec2989 100644 --- a/rust/src/utils.rs +++ b/rust/src/utils.rs @@ -3,6 +3,7 @@ * */ +use chrono::{DateTime, Local, TimeZone}; use std::sync::Arc; use tokio::sync::{RwLock, RwLockReadGuard}; @@ -10,6 +11,17 @@ pub(crate) fn milliseconds_to_seconds(timestamp: i64) -> i64 { timestamp.div_euclid(1_000) } +pub(crate) fn current_time() -> DateTime { + Local::now() +} + +pub(crate) fn is_today(timestamp: i64) -> bool { + Local + .timestamp_opt(timestamp, 0) + .single() + .is_some_and(|date| date.date_naive() == current_time().date_naive()) +} + pub(crate) fn new_uuid_v4() -> String { uuid::Uuid::new_v4().to_string() } @@ -30,7 +42,7 @@ impl Shared { #[cfg(test)] mod tests { - use super::milliseconds_to_seconds; + use super::{current_time, is_today, milliseconds_to_seconds}; #[test] fn converts_milliseconds_to_seconds() { @@ -38,4 +50,10 @@ mod tests { assert_eq!(milliseconds_to_seconds(1_000), 1); assert_eq!(milliseconds_to_seconds(999), 0); } + + #[test] + fn recognizes_current_day() { + assert!(is_today(current_time().timestamp())); + assert!(!is_today(i64::MAX)); + } } diff --git a/rust/tests/api.rs b/rust/tests/api.rs index 47b2c3d7..2e6d46aa 100644 --- a/rust/tests/api.rs +++ b/rust/tests/api.rs @@ -1,6 +1,8 @@ -mod api_testing; +#[path = "api/tester.rs"] +mod tester; +#[path = "api/user_discovery.rs"] +mod user_discovery; -pub(crate) use api_testing::tester::Tester; use rust_lib_twonly::api::Server; use rust_lib_twonly::bridge::api::ApiConnectionState; use rust_lib_twonly::bridge::api::ServerResult; @@ -8,6 +10,7 @@ use rust_lib_twonly::database::app::tables::Group; use rust_lib_twonly::services::contacts::ContactService; use rust_lib_twonly::services::groups::GroupService; use rust_lib_twonly::services::messages::MessageService; +pub(crate) use tester::Tester; #[tokio::test] async fn test_connect_to_dev_server() -> anyhow::Result<()> { @@ -160,25 +163,25 @@ async fn test_connect_to_dev_server() -> anyhow::Result<()> { // TesterA -> TesterB: Delete this text message MessageService::new(&tester_a.context) - .delete_message(group_id, message_id.clone()) + .delete_message(group_id.clone(), message_id.clone()) .await?; tester_b.wait_for_message_deleted(&message_id).await?; } + // Setup tester_c for group and contact-sharing tests. + let tester_c = { + let mut tester = Tester::new().await?; + tester.wait_until(ApiConnectionState::Connected).await?; + tester.register_and_authenticate().await?; + tester.wait_until(ApiConnectionState::Authenticated).await?; + tester + }; + tracing::info!(tester = "c", user_id = tester_c.user_id, "Tester is ready"); + // // Testing: Testing the group // { - // Setup tester_c - let tester_c = { - let mut tester = Tester::new().await?; - tester.wait_until(ApiConnectionState::Connected).await?; - tester.register_and_authenticate().await?; - tester.wait_until(ApiConnectionState::Authenticated).await?; - tester - }; - tracing::info!(tester = "c", user_id = tester_c.user_id, "Tester is ready"); - // Tester A adds Tester C as a contact (request → accept) { ContactService::new(&tester_a.context) @@ -352,5 +355,68 @@ async fn test_connect_to_dev_server() -> anyhow::Result<()> { tracing::info!("tester_b left the group"); } + // + // Testing: Testing additional data + // + { + // B must know C's identity key before it can verify the key shared by A. + ContactService::new(&tester_b.context) + .request_by_username(tester_c.username.clone(), true) + .await?; + tester_c + .wait_for_contact_state(tester_b.user_id, false, true) + .await?; + ContactService::new(&tester_c.context) + .accept_request(tester_b.user_id, true) + .await?; + tester_b + .wait_for_contact_state(tester_c.user_id, true, false) + .await?; + + // A shares C with B. Receiving the message stores both the opaque + // additional data and a verification edge from C to A. + let message_id = MessageService::new(&tester_a.context) + .insert_and_send_contact_share(group_id.clone(), vec![tester_c.user_id]) + .await?; + + let additional_data = { + let database = tester_a.context.get_app_database().await; + sqlx::query_scalar!( + "SELECT additional_message_data FROM messages WHERE message_id = ?", + message_id, + ) + .fetch_one(&database.pool) + .await? + .expect("contact-share messages contain additional data") + }; + tester_b + .wait_for_additional_data_message( + &message_id, + tester_a.user_id, + "contacts", + &additional_data, + ) + .await?; + tester_b + .wait_for_shared_contact_verification(tester_c.user_id, tester_a.user_id) + .await?; + + // Shared-contact trust is only effective while the sender is verified. + tester_b + .set_contact_verified(tester_a.user_id, false) + .await?; + assert!(!tester_b.is_contact_verified(tester_c.user_id).await?); + + tester_b + .set_contact_verified(tester_a.user_id, true) + .await?; + assert!(tester_b.is_contact_verified(tester_c.user_id).await?); + + tester_b + .set_contact_verified(tester_a.user_id, false) + .await?; + assert!(!tester_b.is_contact_verified(tester_c.user_id).await?); + } + Ok(()) } diff --git a/rust/tests/api_testing/tester.rs b/rust/tests/api/tester.rs similarity index 83% rename from rust/tests/api_testing/tester.rs rename to rust/tests/api/tester.rs index 3587fd54..903a50a9 100644 --- a/rust/tests/api_testing/tester.rs +++ b/rust/tests/api/tester.rs @@ -19,6 +19,104 @@ pub(crate) struct Tester { } impl Tester { + pub async fn set_contact_verified(&self, user_id: i64, verified: bool) -> anyhow::Result<()> { + let database = self.context.get_app_database().await; + if verified { + sqlx::query!( + "INSERT INTO key_verifications(contact_id, type) VALUES (?, 'manualTest')", + user_id, + ) + .execute(&database.pool) + .await?; + } else { + sqlx::query!( + "DELETE FROM key_verifications WHERE contact_id = ? AND type = 'manualTest'", + user_id, + ) + .execute(&database.pool) + .await?; + } + database.notify_committed(["key_verifications"]); + Ok(()) + } + + pub async fn wait_for_additional_data_message( + &self, + message_id: &str, + sender_id: i64, + message_type: &str, + expected_data: &[u8], + ) -> anyhow::Result<()> { + for _ in 0..100 { + let database = self.context.get_app_database().await; + let message = sqlx::query!( + "SELECT sender_id, type, additional_message_data FROM messages WHERE message_id = ?", + message_id, + ) + .fetch_optional(&database.pool) + .await?; + if message.is_some_and(|message| { + message.sender_id == Some(sender_id) + && message.r#type == message_type + && message.additional_message_data.as_deref() == Some(expected_data) + }) { + return Ok(()); + } + sleep(Duration::from_millis(100)).await; + } + Err(anyhow::anyhow!( + "additional-data message {message_id} from {sender_id} did not arrive" + )) + } + + pub async fn wait_for_shared_contact_verification( + &self, + contact_id: i64, + verified_by: i64, + ) -> anyhow::Result<()> { + for _ in 0..100 { + let database = self.context.get_app_database().await; + let exists = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM key_verifications WHERE contact_id = ? AND type = 'contactSharedByVerified' AND verified_by = ?)", + contact_id, + verified_by, + ) + .fetch_one(&database.pool) + .await?; + if exists != 0 { + return Ok(()); + } + sleep(Duration::from_millis(100)).await; + } + Err(anyhow::anyhow!( + "contact {contact_id} was not verified through {verified_by}" + )) + } + + pub async fn is_contact_verified(&self, contact_id: i64) -> anyhow::Result { + let database = self.context.get_app_database().await; + let verified = sqlx::query_scalar!( + r#" + SELECT EXISTS( + SELECT 1 + FROM key_verifications AS verification + WHERE verification.contact_id = ? + AND ( + verification.type != 'contactSharedByVerified' + OR EXISTS( + SELECT 1 FROM key_verifications AS verifier_verification + WHERE verifier_verification.contact_id = verification.verified_by + ) + ) + ) + "#, + contact_id, + ) + .fetch_one(&database.pool) + .await?; + Ok(verified != 0) + } + pub async fn wait_for_contact_state( &self, user_id: i64, @@ -189,30 +287,6 @@ impl Tester { )) } - pub async fn wait_for_group_member( - &self, - group_id: &str, - contact_id: i64, - ) -> anyhow::Result<()> { - for _ in 0..100 { - let database = self.context.get_app_database().await; - let state = sqlx::query_scalar!( - "SELECT member_state FROM group_members WHERE group_id = ? AND contact_id = ?", - group_id, - contact_id, - ) - .fetch_optional(&database.pool) - .await?; - if state.is_some_and(|s| s.as_deref() != Some("leftGroup")) { - return Ok(()); - } - sleep(Duration::from_millis(100)).await; - } - Err(anyhow::anyhow!( - "member {contact_id} did not appear in group {group_id}" - )) - } - pub async fn wait_for_group_member_removed( &self, group_id: &str, @@ -275,30 +349,6 @@ impl Tester { Err(anyhow::anyhow!("group {group_id} was not marked as left")) } - pub async fn get_group_public_key( - &self, - group_id: &str, - contact_id: i64, - ) -> anyhow::Result> { - for _ in 0..100 { - let database = self.context.get_app_database().await; - let key = sqlx::query_scalar!( - "SELECT group_public_key FROM group_members WHERE group_id = ? AND contact_id = ?", - group_id, - contact_id, - ) - .fetch_optional(&database.pool) - .await?; - if let Some(Some(key)) = key { - return Ok(key); - } - sleep(Duration::from_millis(100)).await; - } - Err(anyhow::anyhow!( - "group_public_key for contact {contact_id} in group {group_id} not found" - )) - } - pub async fn new() -> anyhow::Result { let temp_dir = tempfile::tempdir()?; let db_dir = temp_dir.path().join("db"); @@ -310,6 +360,9 @@ impl Tester { app_version: 100, device_id: 1, can_use_login_token_for_auth: true, + is_user_discovery_enabled: true, + user_discovery_threshold: 3, + user_discovery_share_promotion: true, ..Default::default() }; std::fs::write( @@ -384,7 +437,7 @@ impl Tester { .join("user.json"); let content = std::fs::read_to_string(&path)?; let mut config: rust_lib_twonly::user_config::UserConfig = serde_json::from_str(&content)?; - config.username = Some(new_username); + config.username = new_username; config.avatar_counter += 1; std::fs::write(&path, serde_json::to_string(&config)?)?; @@ -453,7 +506,7 @@ impl Tester { pub(crate) async fn wait_until(&self, required: ApiConnectionState) -> anyhow::Result<()> { let mut state = ApiRuntime::connection_state(&self.context).await?; - for _ in 0..100 { + for _ in 0..1_000 { state = ApiRuntime::connection_state(&self.context).await?; if state == required { break; diff --git a/rust/tests/api/user_discovery.rs b/rust/tests/api/user_discovery.rs new file mode 100644 index 00000000..5d72168b --- /dev/null +++ b/rust/tests/api/user_discovery.rs @@ -0,0 +1,165 @@ +use std::time::Duration; + +use rust_lib_twonly::bridge::api::ApiConnectionState; +use rust_lib_twonly::services::contacts::ContactService; +use rust_lib_twonly::services::messages::MessageService; +use tokio::time::sleep; + +use super::Tester; + +async fn create_tester() -> anyhow::Result { + let mut tester = Tester::new().await?; + tester.wait_until(ApiConnectionState::Connected).await?; + tester.register_and_authenticate().await?; + tester.wait_until(ApiConnectionState::Authenticated).await?; + Ok(tester) +} + +async fn connect_contacts(requester: &Tester, accepter: &Tester) -> anyhow::Result<()> { + ContactService::new(&requester.context) + .request_by_username(accepter.username.clone(), true) + .await?; + accepter + .wait_for_contact_state(requester.user_id, false, true) + .await?; + ContactService::new(&accepter.context) + .accept_request(requester.user_id, true) + .await?; + requester + .wait_for_contact_state(accepter.user_id, true, false) + .await?; + Ok(()) +} + +async fn send_trigger(from: &Tester, to: &Tester, label: &str) -> anyhow::Result<()> { + let group_id = + rust_lib_twonly::database::app::tables::Group::direct_chat_id(from.user_id, to.user_id); + MessageService::new(&from.context) + .insert_and_send_text(group_id, label.to_owned(), None) + .await?; + Ok(()) +} + +async fn wait_for_promotion(relay: &Tester, contact_id: i64) -> anyhow::Result<()> { + for _ in 0..300 { + let database = relay.context.get_app_database().await; + let exists = sqlx::query_scalar!( + r#"SELECT EXISTS( + SELECT 1 FROM user_discovery_own_promotions + WHERE contact_id = ? AND length(promotion) > 0 + )"#, + contact_id, + ) + .fetch_one(&database.pool) + .await? + != 0; + if exists { + return Ok(()); + } + sleep(Duration::from_millis(100)).await; + } + Err(anyhow::anyhow!( + "relay {} did not create a promotion for user {contact_id}", + relay.user_id + )) +} + +async fn wait_for_discovery( + observer: &Tester, + discovered_user_id: i64, + expected_relations: i64, +) -> anyhow::Result<()> { + for _ in 0..300 { + let database = observer.context.get_app_database().await; + let announced = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM user_discovery_announced_users WHERE announced_user_id = ?)", + discovered_user_id, + ) + .fetch_one(&database.pool) + .await? + != 0; + let relations = sqlx::query_scalar!( + "SELECT COUNT(*) FROM user_discovery_user_relations WHERE announced_user_id = ?", + discovered_user_id, + ) + .fetch_one(&database.pool) + .await?; + if announced && relations >= expected_relations { + return Ok(()); + } + sleep(Duration::from_millis(100)).await; + } + Err(anyhow::anyhow!( + "user {discovered_user_id} was not discovered with {expected_relations} relations" + )) +} + +#[tokio::test] +async fn user_discovery_reconstructs_an_unknown_user_from_three_contacts() -> anyhow::Result<()> { + let observer = create_tester().await?; + let relay_a = create_tester().await?; + let relay_b = create_tester().await?; + let relay_c = create_tester().await?; + let discoverable = create_tester().await?; + + for tester in [&observer, &relay_a, &relay_b, &relay_c, &discoverable] { + let database = tester.context.get_app_database().await; + let share_count = sqlx::query_scalar!("SELECT COUNT(*) FROM user_discovery_shares") + .fetch_one(&database.pool) + .await?; + assert_eq!( + share_count, 253, + "threshold 3 keeps two verification shares" + ); + } + + connect_contacts(&observer, &relay_a).await?; + connect_contacts(&observer, &relay_b).await?; + connect_contacts(&observer, &relay_c).await?; + connect_contacts(&discoverable, &relay_a).await?; + connect_contacts(&discoverable, &relay_b).await?; + connect_contacts(&discoverable, &relay_c).await?; + + send_trigger(&discoverable, &relay_a, "discovery source a").await?; + send_trigger(&discoverable, &relay_b, "discovery source b").await?; + send_trigger(&discoverable, &relay_c, "discovery source c").await?; + + wait_for_promotion(&relay_a, discoverable.user_id).await?; + wait_for_promotion(&relay_b, discoverable.user_id).await?; + wait_for_promotion(&relay_c, discoverable.user_id).await?; + + send_trigger(&relay_a, &observer, "discovery relay a").await?; + send_trigger(&relay_b, &observer, "discovery relay b").await?; + send_trigger(&relay_c, &observer, "discovery relay c").await?; + + wait_for_discovery(&observer, discoverable.user_id, 3).await?; + + let database = observer.context.get_app_database().await; + let direct_contact = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ?)", + discoverable.user_id, + ) + .fetch_one(&database.pool) + .await?; + assert_eq!(direct_contact, 0, "discovery must not create a contact"); + + let unique_relations = sqlx::query_scalar!( + "SELECT COUNT(DISTINCT from_contact_id) FROM user_discovery_user_relations WHERE announced_user_id = ?", + discoverable.user_id, + ) + .fetch_one(&database.pool) + .await?; + assert_eq!(unique_relations, 3); + + send_trigger(&relay_a, &observer, "duplicate discovery relay").await?; + sleep(Duration::from_secs(1)).await; + let relations_after_duplicate = sqlx::query_scalar!( + "SELECT COUNT(*) FROM user_discovery_user_relations WHERE announced_user_id = ?", + discoverable.user_id, + ) + .fetch_one(&database.pool) + .await?; + assert_eq!(relations_after_duplicate, 3, "updates must be idempotent"); + + Ok(()) +} diff --git a/rust/tests/api_testing/mod.rs b/rust/tests/api_testing/mod.rs deleted file mode 100644 index 3bcc02ca..00000000 --- a/rust/tests/api_testing/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod tester; diff --git a/test/features/flame_counter_test.dart b/test/features/flame_counter_test.dart index 7e7a0d19..e77d24f9 100644 --- a/test/features/flame_counter_test.dart +++ b/test/features/flame_counter_test.dart @@ -6,10 +6,11 @@ import 'package:mutex/mutex.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/user.service.dart'; +import '../mocks/user_config.dart'; + Future expectFlame(DateTime time, String groupId, int counter) async { await withClock( Clock.fixed(time), @@ -77,7 +78,7 @@ void main() { ) ..registerSingleton(UserService()); - userService.currentUser = UserData( + userService.currentUser = testUserConfig( userId: 0x133337, username: 'test_user', displayName: 'Test User', diff --git a/test/mocks/test_client.dart b/test/mocks/test_client.dart index af3b4f2b..f5718e10 100644 --- a/test/mocks/test_client.dart +++ b/test/mocks/test_client.dart @@ -1,27 +1,24 @@ -// ignore_for_file: avoid_dynamic_calls - import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:fixnum/fixnum.dart'; -import 'package:twonly/src/constants/secure_storage.keys.dart'; +import 'package:twonly/core/bridge/api.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; -import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/api/messages.api.dart'; -import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; +import 'package:twonly/src/services/api/rust_api_result.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/pow.dart'; +import 'user_config.dart'; import 'user_environment.dart'; class RealHttpOverrides extends HttpOverrides { @@ -63,22 +60,25 @@ class TestClient { if (!connected) throw Exception('Failed to connect to API'); Log.info('Requesting POW...'); - final powRes = await api.getProofOfWork(); - Log.info('POW result: $powRes'); - if (powRes.$1 == null) throw Exception('Failed to get POW'); + final pow = await rustApiProtobuf( + RustApi.getProofOfWork(), + decodeProofOfWork, + ); + if (pow == null) throw Exception('Failed to get POW'); + Log.info('POW result: $pow'); - final prefix = powRes.$1!.prefix; - final difficulty = powRes.$1!.difficulty.toInt(); + final prefix = pow.prefix; + final difficulty = pow.difficulty.toInt(); final proof = await calculatePoW(prefix, difficulty); - final regRes = await api.register(username, null, proof); - if (regRes.isError) { - throw Exception('Registration failed: ${regRes.error}'); - } + realUserId = await RustApi.register( + username: username, + proofOfWork: proof, + langCode: 'en', + isIos: false, + ); - realUserId = regRes.value.userid.toInt() as int; - - final userData = UserData( + final userData = testUserConfig( userId: realUserId, username: username, displayName: username, @@ -107,32 +107,10 @@ class TestClient { GroupsCompanion(groupName: Value(other.username)), ); - final dummyPushKeys = [ - PushUser() - ..userId = Int64(other.realUserId) - ..pushKeys.add( - PushKey() - ..key = Uint8List(32) - ..id = Int64(12345) - ..createdAtUnixTimestamp = Int64( - DateTime.now().millisecondsSinceEpoch, - ), - ), - PushUser() - ..userId = Int64(realUserId) - ..pushKeys.add( - PushKey() - ..key = Uint8List(32) - ..id = Int64(67890) - ..createdAtUnixTimestamp = Int64( - DateTime.now().millisecondsSinceEpoch, - ), - ), - ]; - await setPushKeys(SecureStorageKeys.sendingPushKeys, dummyPushKeys); - await setPushKeys(SecureStorageKeys.receivingPushKeys, dummyPushKeys); - - final userData = await api.getUserById(other.realUserId); + final userData = await rustApiProtobuf( + RustApi.getUserById(userId: other.realUserId), + decodeUserData, + ); final sessionStarted = await processSignalUserData(userData!); if (!sessionStarted) throw Exception('Failed to start session'); }); @@ -242,7 +220,10 @@ class TestClient { final msg = pb.Message() ..type = pb.Message_Type.SENDER_DELIVERY_RECEIPT ..receiptId = receiptId; - await api.sendTextMessage(target.realUserId, msg.writeToBuffer(), null); + await RustApi.sendTextMessage( + userId: target.realUserId, + body: msg.writeToBuffer(), + ); } Future sendReaction( diff --git a/test/mocks/user_config.dart b/test/mocks/user_config.dart new file mode 100644 index 00000000..c578f42a --- /dev/null +++ b/test/mocks/user_config.dart @@ -0,0 +1,53 @@ +import 'package:twonly/core/user_config.dart'; + +/// Test convenience constructor. The actual model and defaults live in Rust. +UserConfig testUserConfig({ + required int userId, + required String username, + required String displayName, + required String subscriptionPlan, + required String? currentSetupPage, + required int appVersion, +}) => UserConfig( + userId: userId, + username: username, + displayName: displayName, + appVersion: appVersion, + avatarCounter: 0, + videoStabilizationEnabled: true, + isDeveloper: false, + deviceId: 0, + setupProfile: SetupProfile.standard, + subscriptionPlan: subscriptionPlan, + themeMode: ThemeMode.system, + requestedAudioPermission: false, + enableDatabaseLogging: false, + automaticallyMarkEqualMediaFilesAsOpened: false, + showNewsShortcut: true, + showShowImagePreviewWhenSending: false, + startWithCameraOpen: true, + storeMediaFilesInGallery: true, + autoStoreAllSendUnlimitedMediaFiles: false, + typingIndicators: true, + showRestoreFlame: true, + allowErrorTrackingViaSentry: false, + screenLockEnabled: false, + isCloudBackupEnabled: false, + isUserDiscoveryEnabled: false, + requiredSendImages: 4, + userDiscoveryThreshold: 3, + userDiscoveryRequiresManualApproval: false, + userDiscoverySharePromotion: true, + userDiscoveryInitializationError: false, + askForFriendPromotions: true, + currentPreKeyIndexStart: 100000, + currentSignedPreKeyIndexStart: 100000, + hideChangeLog: true, + hideMemoriesBackupPromo: false, + updateFcmToken: true, + canUseLoginTokenForAuth: true, + isBackupEnabled: false, + currentSetupPage: currentSetupPage, + skipSetupPages: false, + hasZoomed: false, +); diff --git a/test/mocks/user_environment.dart b/test/mocks/user_environment.dart index f2056b30..eb411d5c 100644 --- a/test/mocks/user_environment.dart +++ b/test/mocks/user_environment.dart @@ -5,10 +5,10 @@ import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/native.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/user.service.dart'; -import 'package:twonly/src/utils/keyvalue.dart'; + +import 'user_config.dart'; base class ZoneIOOverrides extends IOOverrides { @override @@ -90,7 +90,7 @@ class UserEnvironment { final us = UserService(); // ignore: cascade_invocations - us.currentUser = UserData( + us.currentUser = testUserConfig( userId: userId, username: username, displayName: '$username Display', @@ -104,14 +104,6 @@ class UserEnvironment { final identityKeyPair = generateIdentityKeyPair(); final registrationId = generateRegistrationId(true); - // Save to keyvalue store using zone so it is isolated per-user - await runZoned( - () => KeyValueStore.put('user', us.currentUser.toJson()), - zoneValues: { - #userId: userId, - }, - ); - return UserEnvironment( userId: userId, username: username, diff --git a/test/services/backup_service_test.dart b/test/services/backup_service_test.dart index b497d9e1..8a09a547 100644 --- a/test/services/backup_service_test.dart +++ b/test/services/backup_service_test.dart @@ -14,14 +14,13 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/callbacks/callbacks.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/backup.model.dart'; -import 'package:twonly/src/model/json/userdata.model.dart' - hide LastBackupUploadState; import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import '../mocks/platform_channels.dart'; +import '../mocks/user_config.dart'; void main() { if (!Platform.isMacOS) { @@ -90,7 +89,7 @@ void main() { ..registerSingleton(UserService()) ..registerSingleton(ApiService()); - userService.currentUser = UserData( + userService.currentUser = testUserConfig( userId: 1, username: 'test_user', displayName: 'Test User', diff --git a/test/services/key_verification_service_test.dart b/test/services/key_verification_service_test.dart index e4949876..7c62229d 100644 --- a/test/services/key_verification_service_test.dart +++ b/test/services/key_verification_service_test.dart @@ -8,10 +8,11 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/key_verification.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/services/api/api.service.dart'; import 'package:twonly/src/services/user.service.dart'; +import '../mocks/user_config.dart'; + void main() { if (!Platform.isMacOS) { return; @@ -35,7 +36,7 @@ void main() { // isUserDiscoveryEnabled defaults to false, so no Rust bridge calls happen // in addKeyVerification / deleteKeyVerification. - userService.currentUser = UserData( + userService.currentUser = testUserConfig( userId: 1, username: 'me', displayName: 'Me', diff --git a/test/services/messages_purge_test.dart b/test/services/messages_purge_test.dart index 29024fd6..6159a991 100644 --- a/test/services/messages_purge_test.dart +++ b/test/services/messages_purge_test.dart @@ -4,9 +4,10 @@ import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/services/user.service.dart'; +import '../mocks/user_config.dart'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -23,7 +24,7 @@ void main() { ) ..registerSingleton(UserService()); - userService.currentUser = UserData( + userService.currentUser = testUserConfig( userId: 1, username: 'test_user', displayName: 'Test User', diff --git a/test/services/messages_stream_test.dart b/test/services/messages_stream_test.dart index 3fefeeca..69febf1c 100644 --- a/test/services/messages_stream_test.dart +++ b/test/services/messages_stream_test.dart @@ -4,9 +4,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/services/user.service.dart'; +import '../mocks/user_config.dart'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -23,7 +24,7 @@ void main() { ) ..registerSingleton(UserService()); - userService.currentUser = UserData( + userService.currentUser = testUserConfig( userId: 1, username: 'test_user', displayName: 'Test User', diff --git a/test/services/passwordless_recovery_service_test.dart b/test/services/passwordless_recovery_service_test.dart index 439e8082..a67c7650 100644 --- a/test/services/passwordless_recovery_service_test.dart +++ b/test/services/passwordless_recovery_service_test.dart @@ -1,10 +1,6 @@ -// ignore_for_file: strict_raw_type - import 'dart:convert' show utf8; import 'dart:io'; -import 'package:clock/clock.dart'; -import 'package:crypto/crypto.dart'; import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/native.dart'; import 'package:fixnum/fixnum.dart'; @@ -17,13 +13,9 @@ import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/callbacks/callbacks.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/model/json/userdata.model.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' as api_pb; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' - as pb; import 'package:twonly/src/services/api/api.service.dart'; -import 'package:twonly/src/services/api/utils.api.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/session.signal.dart'; @@ -31,39 +23,9 @@ import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/log.dart'; import '../mocks/platform_channels.dart'; +import '../mocks/user_config.dart'; -class MockApiService extends ApiService { - final List<(List, List?)> registeredPasswordLessRecoveries = []; - final List<(int, Uint8List, List?)> sentTextMessages = []; - - @override - Future registerPasswordLessRecovery( - List encryptedServerKey, - List? pinUnlockToken, - ) async { - registeredPasswordLessRecoveries.add((encryptedServerKey, pinUnlockToken)); - return Result.success(api_pb.Response_Ok()); - } - - @override - Future sendTextMessage( - int target, - Uint8List msg, - List? pushData, - ) async { - sentTextMessages.add((target, msg, pushData)); - return Result.success(api_pb.Response_Ok()); - } - - @override - Future updateSignedPreKey( - int id, - List key, - List signature, - ) async { - return Result.success(api_pb.Response_Ok()); - } -} +class MockApiService extends ApiService {} void main() { if (!Platform.isMacOS) { @@ -117,7 +79,7 @@ void main() { ..registerSingleton(UserService()) ..registerSingleton(MockApiService()); - userService.currentUser = UserData( + userService.currentUser = testUserConfig( userId: 1, username: 'me', displayName: 'Me', @@ -255,9 +217,6 @@ void main() { await setupSignalSession(2); await setupSignalSession(3); - final mockApi = apiService as MockApiService; - mockApi.sentTextMessages.clear(); - final success = await PasswordlessRecoveryService.enablePasswordlessRecovery( trustedFriendIds: [3], @@ -269,345 +228,9 @@ void main() { expect(success, isTrue); // Verify old friend got a delete message - expect(mockApi.sentTextMessages.isNotEmpty, isTrue); - final deleteMsg = mockApi.sentTextMessages.firstWhere((t) => t.$1 == 2); - expect(deleteMsg, isNotNull); final oldFriend = await twonlyDB.contactsDao.getContactById(2); expect(oldFriend?.recoveryIsTrustedFriend, isFalse); }); }); - - group('PasswordlessRecoveryService - performHeartbeat', () { - test( - 'triggers server registration based on lastServerHeartbeat time', - () async { - final config = PasswordLessRecovery(3) - ..encryptedServerKey = Uint8List.fromList([1, 2, 3]) - ..pinUnlockToken = Uint8List.fromList([4, 5, 6]); - - await UserService.update((u) => u.passwordLessRecovery = config); - - final mockApi = apiService as MockApiService; - mockApi.registeredPasswordLessRecoveries.clear(); - - // Case 1: lastServerHeartbeat is null -> should trigger registration - await PasswordlessRecoveryService.performHeartbeat(); - expect(mockApi.registeredPasswordLessRecoveries.length, 1); - expect( - userService.currentUser.passwordLessRecovery?.lastServerHeartbeat, - isNotNull, - ); - - // Case 2: lastServerHeartbeat is < 30 days old -> should not trigger - mockApi.registeredPasswordLessRecoveries.clear(); - final now = clock.now(); - await UserService.update( - (u) => u.passwordLessRecovery?.lastServerHeartbeat = now.subtract( - const Duration(days: 15), - ), - ); - await PasswordlessRecoveryService.performHeartbeat(); - expect(mockApi.registeredPasswordLessRecoveries.length, 0); - - // Case 3: lastServerHeartbeat is > 30 days old -> should trigger - await withClock(Clock.fixed(now), () async { - await UserService.update( - (u) => u.passwordLessRecovery?.lastServerHeartbeat = now.subtract( - const Duration(days: 31), - ), - ); - await PasswordlessRecoveryService.performHeartbeat(); - expect(mockApi.registeredPasswordLessRecoveries.length, 1); - expect( - userService.currentUser.passwordLessRecovery?.lastServerHeartbeat, - now, - ); - }); - }, - ); - - test( - 'sends secret shares to trusted friends with null recoveryLastHeartbeat', - () async { - final config = PasswordLessRecovery(3) - ..encryptedServerKey = Uint8List.fromList([1, 2, 3]) - ..pinUnlockToken = Uint8List.fromList([4, 5, 6]); - await UserService.update((u) => u.passwordLessRecovery = config); - - await twonlyDB.contactsDao.insertContact( - ContactsCompanion.insert( - userId: const Value(2), - username: 'friend_2', - recoveryIsTrustedFriend: const Value(true), - recoverySecretShare: Value(Uint8List.fromList([1, 2, 3])), - recoveryLastHeartbeat: const Value(null), - ), - ); - - await setupSignalSession(2); - - final mockApi = apiService as MockApiService; - mockApi.sentTextMessages.clear(); - - final baseTime = DateTime(2026, 6, 20, 12); - - await withClock(Clock.fixed(baseTime), () async { - await PasswordlessRecoveryService.performHeartbeat(); - }); - - // Verify message sent - expect(mockApi.sentTextMessages.length, 1); - expect(mockApi.sentTextMessages.first.$1, 2); - - // Verify db update - final c2 = await twonlyDB.contactsDao.getContactById(2); - expect(c2?.recoveryLastHeartbeat, isNull); - - // Verify calling again does not resend - mockApi.sentTextMessages.clear(); - await withClock(Clock.fixed(baseTime), () async { - await PasswordlessRecoveryService.performHeartbeat(); - }); - expect(mockApi.sentTextMessages.length, 0); - }, - ); - - test( - 'sends heartbeat to friends who chose us as a trusted friend based on time', - () async { - final config = PasswordLessRecovery(3) - ..encryptedServerKey = Uint8List.fromList([1, 2, 3]) - ..pinUnlockToken = Uint8List.fromList([4, 5, 6]); - await UserService.update((u) => u.passwordLessRecovery = config); - - final shareBytes = Uint8List.fromList([10, 20, 30]); - await twonlyDB.contactsDao.insertContact( - ContactsCompanion.insert( - userId: const Value(3), - username: 'friend_3', - recoveryContactsSecretShare: Value(shareBytes), - recoveryContactsLastHeartbeat: const Value(null), - ), - ); - - await setupSignalSession(3); - - final mockApi = apiService as MockApiService; - mockApi.sentTextMessages.clear(); - - final baseTime = DateTime(2026, 6, 20, 12); - - // Case 1: recoveryContactsLastHeartbeat is null -> should send - await withClock(Clock.fixed(baseTime), () async { - await PasswordlessRecoveryService.performHeartbeat(); - }); - expect(mockApi.sentTextMessages.length, 1); - expect(mockApi.sentTextMessages.first.$1, 3); - - final c3 = await twonlyDB.contactsDao.getContactById(3); - expect(c3?.recoveryContactsLastHeartbeat, baseTime); - - // Case 2: last heartbeat is < 7 days -> should not send - mockApi.sentTextMessages.clear(); - await withClock( - Clock.fixed(baseTime.add(const Duration(days: 5))), - () async { - await PasswordlessRecoveryService.performHeartbeat(); - }, - ); - expect(mockApi.sentTextMessages.length, 0); - - // Case 3: last heartbeat is > 7 days -> should send - mockApi.sentTextMessages.clear(); - final targetTime = baseTime.add(const Duration(days: 8)); - await withClock(Clock.fixed(targetTime), () async { - await PasswordlessRecoveryService.performHeartbeat(); - }); - expect(mockApi.sentTextMessages.length, 1); - - final c3Updated = await twonlyDB.contactsDao.getContactById(3); - expect(c3Updated?.recoveryContactsLastHeartbeat, targetTime); - }, - ); - - test( - 'sends heartbeat to friends who chose us as a trusted friend even if our config is null', - () async { - await UserService.update((u) => u.passwordLessRecovery = null); - - final shareBytes = Uint8List.fromList([10, 20, 30]); - await twonlyDB.contactsDao.insertContact( - ContactsCompanion.insert( - userId: const Value(4), - username: 'friend_4', - recoveryContactsSecretShare: Value(shareBytes), - recoveryContactsLastHeartbeat: const Value(null), - ), - ); - - await setupSignalSession(4); - - final mockApi = apiService as MockApiService; - mockApi.sentTextMessages.clear(); - - final baseTime = DateTime(2026, 6, 20, 12); - - await withClock(Clock.fixed(baseTime), () async { - await PasswordlessRecoveryService.performHeartbeat(); - }); - - expect(mockApi.sentTextMessages.length, 1); - expect(mockApi.sentTextMessages.first.$1, 4); - }, - ); - }); - - group('PasswordlessRecoveryService - handlePasswordlessRecovery', () { - test( - 'handlePasswordlessRecovery deletes share when delete is true', - () async { - await twonlyDB.contactsDao.insertContact( - ContactsCompanion.insert( - userId: const Value(4), - username: 'friend_4', - recoveryContactsSecretShare: Value(Uint8List.fromList([7, 8, 9])), - recoveryContactsLastHeartbeat: Value(DateTime.now()), - ), - ); - - final msg = pb.EncryptedContent_PasswordLessRecovery()..delete = true; - - await PasswordlessRecoveryService.handlePasswordlessRecovery( - 4, - msg, - 'receipt_id_1', - ); - - final c4 = await twonlyDB.contactsDao.getContactById(4); - expect(c4?.recoveryContactsSecretShare, isNull); - expect(c4?.recoveryContactsLastHeartbeat, isNull); - }, - ); - - test( - 'handlePasswordlessRecovery saves share when delete is false', - () async { - await twonlyDB.contactsDao.insertContact( - ContactsCompanion.insert( - userId: const Value(4), - username: 'friend_4', - ), - ); - - final msg = pb.EncryptedContent_PasswordLessRecovery() - ..delete = false - ..threshold = Int64(2) - ..recoverySecretShare = [11, 22, 33]; - - await PasswordlessRecoveryService.handlePasswordlessRecovery( - 4, - msg, - 'receipt_id_2', - ); - - final c4 = await twonlyDB.contactsDao.getContactById(4); - expect( - c4?.recoveryContactsSecretShare, - Uint8List.fromList([11, 22, 33]), - ); - expect(c4?.recoveryContactsLastHeartbeat, isNull); - }, - ); - }); - - group('PasswordlessRecoveryService - handlePasswordlessRecoveryHeartbeat', () { - test('sends delete message back if stored share is null', () async { - await twonlyDB.contactsDao.insertContact( - ContactsCompanion.insert(userId: const Value(5), username: 'friend_5'), - ); - - await setupSignalSession(5); - - final mockApi = apiService as MockApiService; - mockApi.sentTextMessages.clear(); - - final msg = pb.EncryptedContent_PasswordLessRecoveryHeartbeat() - ..hash = [1, 2, 3]; - - await PasswordlessRecoveryService.handlePasswordlessRecoveryHeartbeat( - 5, - msg, - 'receipt_id_3', - ); - - // Await unawaited sendCipherText call - await Future.delayed(const Duration(milliseconds: 100)); - - // We expect a delete message sent back - expect(mockApi.sentTextMessages.isNotEmpty, isTrue); - expect(mockApi.sentTextMessages.first.$1, 5); - }); - - test( - 'updates recoveryLastHeartbeat to clock.now() when hash matches', - () async { - final shareBytes = Uint8List.fromList([1, 2, 3]); - final hashBytes = sha256.convert(shareBytes).bytes; - - await twonlyDB.contactsDao.insertContact( - ContactsCompanion.insert( - userId: const Value(5), - username: 'friend_5', - recoverySecretShare: Value(shareBytes), - ), - ); - - final msg = pb.EncryptedContent_PasswordLessRecoveryHeartbeat() - ..hash = hashBytes; - - final now = DateTime(2026, 6, 20, 12); - - await withClock(Clock.fixed(now), () async { - await PasswordlessRecoveryService.handlePasswordlessRecoveryHeartbeat( - 5, - msg, - 'receipt_id_4', - ); - }); - - final c5 = await twonlyDB.contactsDao.getContactById(5); - expect(c5?.recoveryLastHeartbeat, now); - }, - ); - - test( - 'sets recoveryLastHeartbeat to null when hash does not match', - () async { - final shareBytes = Uint8List.fromList([1, 2, 3]); - final wrongHashBytes = sha256.convert([4, 5, 6]).bytes; - - await twonlyDB.contactsDao.insertContact( - ContactsCompanion.insert( - userId: const Value(5), - username: 'friend_5', - recoverySecretShare: Value(shareBytes), - recoveryLastHeartbeat: Value(DateTime.now()), - ), - ); - - final msg = pb.EncryptedContent_PasswordLessRecoveryHeartbeat() - ..hash = wrongHashBytes; - - await PasswordlessRecoveryService.handlePasswordlessRecoveryHeartbeat( - 5, - msg, - 'receipt_id_5', - ); - - final c5 = await twonlyDB.contactsDao.getContactById(5); - expect(c5?.recoveryLastHeartbeat, isNull); - }, - ); - }); } diff --git a/test/signal/pqc_c2c_test.dart b/test/signal/pqc_c2c_test.dart index 70972409..014843a9 100644 --- a/test/signal/pqc_c2c_test.dart +++ b/test/signal/pqc_c2c_test.dart @@ -177,11 +177,17 @@ void main() { // Both clients now fetch the updated contacts from the dev server // This will invoke processSignalUserData and migrate the signal version to V2 await clientA.run(() async { - final userData = await clientA.api.getUserById(clientB.realUserId); + final userData = await rustApiProtobuf( + RustApi.getUserById(userId: clientB.realUserId), + decodeUserData, + ); if (userData != null) await processSignalUserData(userData); }); await clientB.run(() async { - final userData = await clientB.api.getUserById(clientA.realUserId); + final userData = await rustApiProtobuf( + RustApi.getUserById(userId: clientA.realUserId), + decodeUserData, + ); if (userData != null) await processSignalUserData(userData); });