diff --git a/README.md b/README.md index faee6c74..02894d5b 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,6 @@ If you decide to give twonly a try, please keep in mind that it is still in its - Importing memories from Snapchat - For Android: Support for [UnifiedPush] (https://unifiedpush.org/) - For Android: Reproducible builds -- Implementation of [Sealed Sender](https://signal.org/blog/sealed-sender/) (or a similar protocol) to minimize metadata - Decentralize the server so that anyone can run their own server ## Security Issues diff --git a/android/app/src/main/kotlin/eu/twonly/media/NativeVideoCodec.kt b/android/app/src/main/kotlin/eu/twonly/media/NativeVideoCodec.kt index dba8ef60..67a70b08 100644 --- a/android/app/src/main/kotlin/eu/twonly/media/NativeVideoCodec.kt +++ b/android/app/src/main/kotlin/eu/twonly/media/NativeVideoCodec.kt @@ -10,11 +10,14 @@ import android.os.Handler import android.os.Looper import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes +import androidx.media3.common.OverlaySettings +import androidx.media3.common.util.Size import androidx.media3.common.util.UnstableApi import androidx.media3.effect.BitmapOverlay import androidx.media3.effect.FrameDropEffect import androidx.media3.effect.OverlayEffect import androidx.media3.effect.Presentation +import androidx.media3.effect.StaticOverlaySettings import androidx.media3.effect.TextureOverlay import androidx.media3.transformer.Composition import androidx.media3.transformer.DefaultEncoderFactory @@ -299,13 +302,48 @@ object NativeVideoCodec { val bitmap = BitmapFactory.decodeFile(overlayPath) if (bitmap != null) { val overlays: ImmutableList = - ImmutableList.of(BitmapOverlay.createStaticBitmapOverlay(bitmap)) + ImmutableList.of(FullFrameBitmapOverlay(bitmap)) videoEffects.add(OverlayEffect(overlays)) } } return Effects(ImmutableList.of(), ImmutableList.copyOf(videoEffects)) } + /** + * Stretches the editor's overlay across the whole frame. + * + * Media3 draws a [BitmapOverlay] at the bitmap's own pixel size, centred: + * the default settings only normalise the bitmap against the frame, they do + * not fit it to one. The overlay is captured at the phone's device pixel + * ratio over the video's on-screen rectangle, so it is comfortably larger + * than the 720p a send is normalised to, and drawn as-is it would appear + * enlarged with whatever the user put near an edge cut off. It shares the + * video's aspect ratio, having been drawn over it, so filling the frame + * does not distort it. The iOS renderer scales its overlay onto the render + * size the same way. + * + * The frame size is only settled once every resolution-changing effect + * ahead of this one has run, which is what [configure] reports, so the + * scale is taken from there rather than from the probed source. + */ + private class FullFrameBitmapOverlay(private val bitmap: Bitmap) : BitmapOverlay() { + private var settings: OverlaySettings = StaticOverlaySettings.Builder().build() + + override fun getBitmap(presentationTimeUs: Long): Bitmap = bitmap + + override fun configure(videoSize: Size) { + if (bitmap.width <= 0 || bitmap.height <= 0) return + settings = StaticOverlaySettings.Builder() + .setScale( + videoSize.width.toFloat() / bitmap.width, + videoSize.height.toFloat() / bitmap.height, + ) + .build() + } + + override fun getOverlaySettings(presentationTimeUs: Long): OverlaySettings = settings + } + private fun pollProgress( handler: Handler, transformer: Transformer, diff --git a/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyNotificationWorker.kt b/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyNotificationWorker.kt index dc5f94d6..d476b981 100644 --- a/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyNotificationWorker.kt +++ b/android/app/src/main/kotlin/eu/twonly/notifications/TwonlyNotificationWorker.kt @@ -72,9 +72,8 @@ class TwonlyNotificationWorker( manager: NotificationManagerCompat, addition: NativeNotificationAddition, ): Boolean { - val avatar = addition.avatarPath - ?.let(BitmapFactory::decodeFile) - ?.let(IconCompat::createWithBitmap) + val avatarBitmap = addition.avatarPath?.let(BitmapFactory::decodeFile) + val avatar = avatarBitmap?.let(IconCompat::createWithBitmap) val sender = Person.Builder() .setName(addition.senderName) .setKey(addition.senderId.toString()) @@ -107,6 +106,11 @@ class TwonlyNotificationWorker( .setContentIntent(pendingIntent) .setPriority(NotificationCompat.PRIORITY_HIGH) .setGroup(addition.conversationId ?: addition.senderId.toString()) + // Android only draws the MessagingStyle person icon for conversation + // notifications, which require a long-lived shortcut. Without one the + // standard template is used, where the large icon is the only place + // the sender's avatar can appear. + .apply { avatarBitmap?.let(::setLargeIcon) } .build() return try { manager.notify(nativeNotificationId(addition.notificationId), notification) diff --git a/ios/NotificationService/NotificationService.entitlements b/ios/NotificationService/NotificationService.entitlements index e32e0256..035052c7 100644 --- a/ios/NotificationService/NotificationService.entitlements +++ b/ios/NotificationService/NotificationService.entitlements @@ -4,8 +4,6 @@ com.apple.developer.usernotifications.communication - com.apple.developer.usernotifications.filtering - com.apple.security.application-groups group.eu.twonly.runtime diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index aaf6976b..64f60d21 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -8,9 +8,15 @@ private let runtimeAppGroup = "group.eu.twonly.runtime" private struct NativeNotificationResponse: Decodable { let ok: Bool let batch: NativeNotificationBatch? + let fallback: NativeNotificationPresentation? let error: String? } +private struct NativeNotificationPresentation: Decodable { + let title: String + let body: String +} + private struct NativeNotificationBatch: Decodable { let additions: [NativeNotificationAddition] let removals: [String] @@ -58,28 +64,34 @@ final class NotificationService: UNNotificationServiceExtension { private let finishLock = NSLock() private var hasFinished = false private var contentHandler: ((UNNotificationContent) -> Void)? + private var fallbackContent: UNNotificationContent? override func didReceive( _ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void ) { self.contentHandler = contentHandler + fallbackContent = request.content guard let runtimeDirectory = Self.runtimeDirectory() else { - suppress(reason: "shared runtime directory is unavailable") + deliverFallback(reason: "shared runtime directory is unavailable") return } DispatchQueue.global(qos: .userInitiated).async { guard let response = Self.processWakeup(runtimeDirectory: runtimeDirectory) else { - self.suppress(reason: "notification worker returned no response") + self.deliverFallback(reason: "notification worker returned no response") return } - guard response.ok, - let batch = response.batch, - !batch.additions.isEmpty - else { - self.suppress(reason: response.error ?? "notification worker returned no messages") + guard response.ok else { + self.deliverFallback( + reason: response.error ?? "notification worker failed", + presentation: response.fallback + ) + return + } + guard let batch = response.batch, !batch.additions.isEmpty else { + self.deliverFallback(reason: "notification worker returned no messages") return } self.render(batch: batch, original: request.content) @@ -87,7 +99,7 @@ final class NotificationService: UNNotificationServiceExtension { } override func serviceExtensionTimeWillExpire() { - suppress(reason: "notification service extension timed out") + deliverFallback(reason: "notification service extension timed out") } private func render(batch: NativeNotificationBatch, original: UNNotificationContent) { @@ -208,14 +220,26 @@ final class NotificationService: UNNotificationServiceExtension { } hasFinished = true let handler = contentHandler + fallbackContent = nil contentHandler = nil finishLock.unlock() handler?(content) } - private func suppress(reason: String) { - NSLog("Suppressing Twonly wake-up notification: \(reason)") - finish(with: UNNotificationContent()) + private func deliverFallback( + reason: String, + presentation: NativeNotificationPresentation? = nil + ) { + NSLog("Delivering Twonly wake-up fallback notification: \(reason)") + guard let presentation else { + finish(with: fallbackContent ?? UNMutableNotificationContent()) + return + } + let content = (fallbackContent?.mutableCopy() as? UNMutableNotificationContent) + ?? UNMutableNotificationContent() + content.title = presentation.title + content.body = presentation.body + finish(with: content) } private static func runtimeDirectory() -> String? { diff --git a/lib/core/bridge/callbacks.dart b/lib/core/bridge/callbacks.dart index cd94212f..a4bbbfff 100644 --- a/lib/core/bridge/callbacks.dart +++ b/lib/core/bridge/callbacks.dart @@ -8,17 +8,15 @@ 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` -// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Api`, `FlutterCallbacks`, `Logging` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone` +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Api`, `FlutterCallbacks` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone` Future initFlutterCallbacks({ required int callbackId, - required FutureOr> Function() loggingGetStreamSink, required FutureOr Function(PlatformInt64) apiVerificationSucceeded, required FutureOr Function(UserConfig) apiUserConfigChanged, }) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks( callbackId: callbackId, - loggingGetStreamSink: loggingGetStreamSink, apiVerificationSucceeded: apiVerificationSucceeded, apiUserConfigChanged: apiUserConfigChanged, ); diff --git a/lib/core/bridge/callbacks/log.dart b/lib/core/bridge/callbacks/log.dart deleted file mode 100644 index 42fb1c69..00000000 --- a/lib/core/bridge/callbacks/log.dart +++ /dev/null @@ -1,14 +0,0 @@ -// 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'; - -// These functions are ignored because they are not marked as `pub`: `set_dart_sink` -// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `DartWriter` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `flush`, `make_writer`, `write` - -Future stripAnsi({required String input}) => - RustLib.instance.api.crateBridgeCallbacksLogStripAnsi(input: input); diff --git a/lib/core/bridge/logging.dart b/lib/core/bridge/logging.dart new file mode 100644 index 00000000..6d6a8651 --- /dev/null +++ b/lib/core/bridge/logging.dart @@ -0,0 +1,48 @@ +// 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'; + +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt` + +/// Adds a Dart record to the Rust-owned application log. +/// +/// This only performs a short, serialized append and is synchronous so Dart +/// records cannot be reordered by a collection of unawaited futures. +void writeLog({ + required LogLevel level, + required String source, + required String message, + required bool inBackground, +}) => RustLib.instance.api.crateBridgeLoggingWriteLog( + level: level, + source: source, + message: message, + inBackground: inBackground, +); + +Future loadLogFile() => + RustLib.instance.api.crateBridgeLoggingLoadLogFile(); + +Future readLastLogLines({required int lineCount}) => RustLib + .instance + .api + .crateBridgeLoggingReadLastLogLines(lineCount: lineCount); + +Future cleanLogFile() => + RustLib.instance.api.crateBridgeLoggingCleanLogFile(); + +/// Truncates `app.log` through its owner instead of unlinking an open file. +Future clearLogFile() => + RustLib.instance.api.crateBridgeLoggingClearLogFile(); + +enum LogLevel { + finest, + fine, + info, + warning, + shout, +} diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index ec48b684..88eb47ce 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -7,8 +7,8 @@ import 'api/server/prekeys.dart'; import 'bridge.dart'; import 'bridge/api.dart'; import 'bridge/callbacks.dart'; -import 'bridge/callbacks/log.dart'; import 'bridge/groups.dart'; +import 'bridge/logging.dart'; import 'bridge/user_config.dart'; import 'bridge/wrapper.dart'; import 'bridge/wrapper/app_database.dart'; @@ -86,7 +86,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 640399573; + int get rustContentHash => 174490644; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -107,6 +107,10 @@ abstract class RustLibApi extends BaseApi { required Int64List memberIds, }); + Future crateBridgeLoggingCleanLogFile(); + + Future crateBridgeLoggingClearLogFile(); + Future crateBridgeGroupsCreateNewGroup({ required String groupName, required Int64List memberIds, @@ -142,7 +146,6 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeCallbacksInitFlutterCallbacks({ required int callbackId, - required FutureOr> Function() loggingGetStreamSink, required FutureOr Function(PlatformInt64) apiVerificationSucceeded, required FutureOr Function(UserConfig) apiUserConfigChanged, }); @@ -155,12 +158,16 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeGroupsLeaveGroup({required String groupId}); + Future crateBridgeLoggingLoadLogFile(); + Future crateBridgeGroupsManageAdminState({ required String groupId, required PlatformInt64 contactId, required bool remove, }); + Future crateBridgeLoggingReadLastLogLines({required int lineCount}); + Future crateBridgeGroupsRemoveMemberFromGroup({ required String groupId, required PlatformInt64 contactId, @@ -620,8 +627,6 @@ abstract class RustLibApi extends BaseApi { required int threshold, }); - Future crateBridgeCallbacksLogStripAnsi({required String input}); - Future crateBridgeGroupsUpdateChatDeletionTime({ required String groupId, required PlatformInt64 deleteMessagesAfterMilliseconds, @@ -658,6 +663,13 @@ abstract class RustLibApi extends BaseApi { required UserConfig base, required UserConfig config, }); + + void crateBridgeLoggingWriteLog({ + required LogLevel level, + required String source, + required String message, + required bool inBackground, + }); } class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @@ -736,6 +748,66 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["groupId", "memberIds"], ); + @override + Future crateBridgeLoggingCleanLogFile() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 3, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeLoggingCleanLogFileConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeLoggingCleanLogFileConstMeta => + const TaskConstMeta( + debugName: "clean_log_file", + argNames: [], + ); + + @override + Future crateBridgeLoggingClearLogFile() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 4, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bool, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeLoggingClearLogFileConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeLoggingClearLogFileConstMeta => + const TaskConstMeta( + debugName: "clear_log_file", + argNames: [], + ); + @override Future crateBridgeGroupsCreateNewGroup({ required String groupName, @@ -750,7 +822,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 3, + funcId: 5, port: port_, ); }, @@ -781,7 +853,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 4, + funcId: 6, port: port_, ); }, @@ -811,7 +883,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 5, + funcId: 7, port: port_, ); }, @@ -847,7 +919,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 6, + funcId: 8, port: port_, ); }, @@ -885,7 +957,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 7, + funcId: 9, port: port_, ); }, @@ -921,7 +993,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 8, + funcId: 10, port: port_, ); }, @@ -964,7 +1036,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 9, + funcId: 11, port: port_, ); }, @@ -990,7 +1062,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @override Future crateBridgeCallbacksInitFlutterCallbacks({ required int callbackId, - required FutureOr> Function() loggingGetStreamSink, required FutureOr Function(PlatformInt64) apiVerificationSucceeded, required FutureOr Function(UserConfig) apiUserConfigChanged, }) { @@ -999,10 +1070,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_u_32(callbackId, serializer); - sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException( - loggingGetStreamSink, - serializer, - ); sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( apiVerificationSucceeded, serializer, @@ -1014,7 +1081,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 10, + funcId: 12, port: port_, ); }, @@ -1023,12 +1090,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: null, ), constMeta: kCrateBridgeCallbacksInitFlutterCallbacksConstMeta, - argValues: [ - callbackId, - loggingGetStreamSink, - apiVerificationSucceeded, - apiUserConfigChanged, - ], + argValues: [callbackId, apiVerificationSucceeded, apiUserConfigChanged], apiImpl: this, ), ); @@ -1039,7 +1101,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { debugName: "init_flutter_callbacks", argNames: [ "callbackId", - "loggingGetStreamSink", "apiVerificationSucceeded", "apiUserConfigChanged", ], @@ -1057,7 +1118,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 11, + funcId: 13, port: port_, ); }, @@ -1090,7 +1151,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 12, + funcId: 14, port: port_, ); }, @@ -1121,7 +1182,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 13, + funcId: 15, port: port_, ); }, @@ -1142,6 +1203,36 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["groupId"], ); + @override + Future crateBridgeLoggingLoadLogFile() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 16, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeLoggingLoadLogFileConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeLoggingLoadLogFileConstMeta => + const TaskConstMeta( + debugName: "load_log_file", + argNames: [], + ); + @override Future crateBridgeGroupsManageAdminState({ required String groupId, @@ -1158,7 +1249,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 14, + funcId: 17, port: port_, ); }, @@ -1179,6 +1270,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["groupId", "contactId", "remove"], ); + @override + Future crateBridgeLoggingReadLastLogLines({required int lineCount}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_u_32(lineCount, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 18, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeLoggingReadLastLogLinesConstMeta, + argValues: [lineCount], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateBridgeLoggingReadLastLogLinesConstMeta => + const TaskConstMeta( + debugName: "read_last_log_lines", + argNames: ["lineCount"], + ); + @override Future crateBridgeGroupsRemoveMemberFromGroup({ required String groupId, @@ -1193,7 +1315,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 15, + funcId: 19, port: port_, ); }, @@ -1224,7 +1346,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 16, + funcId: 20, port: port_, ); }, @@ -1257,7 +1379,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 17, + funcId: 21, port: port_, ); }, @@ -1287,7 +1409,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 18, + funcId: 22, port: port_, ); }, @@ -1315,7 +1437,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(protocol, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 19)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 23)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1343,7 +1465,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 24, port: port_, ); }, @@ -1375,7 +1497,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_i_64(contactId, serializer); sse_encode_i_64(profileCounter, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 21)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1404,7 +1526,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 22, + funcId: 26, port: port_, ); }, @@ -1434,7 +1556,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 27, port: port_, ); }, @@ -1475,7 +1597,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 24, + funcId: 28, port: port_, ); }, @@ -1516,7 +1638,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 25, + funcId: 29, port: port_, ); }, @@ -1551,7 +1673,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 26, + funcId: 30, port: port_, ); }, @@ -1583,7 +1705,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 27, + funcId: 31, port: port_, ); }, @@ -1615,7 +1737,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 28, + funcId: 32, port: port_, ); }, @@ -1645,7 +1767,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 29, + funcId: 33, port: port_, ); }, @@ -1675,7 +1797,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 30, + funcId: 34, port: port_, ); }, @@ -1708,7 +1830,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 31, + funcId: 35, port: port_, ); }, @@ -1739,7 +1861,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 32, + funcId: 36, port: port_, ); }, @@ -1769,7 +1891,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(avatarSvgCompressed, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 37)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -1797,7 +1919,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 34, + funcId: 38, port: port_, ); }, @@ -1828,7 +1950,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 35, + funcId: 39, port: port_, ); }, @@ -1858,7 +1980,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 36, + funcId: 40, port: port_, ); }, @@ -1889,7 +2011,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 37, + funcId: 41, port: port_, ); }, @@ -1920,7 +2042,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 38, + funcId: 42, port: port_, ); }, @@ -1950,7 +2072,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 39, + funcId: 43, port: port_, ); }, @@ -1983,7 +2105,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 40, + funcId: 44, port: port_, ); }, @@ -2018,7 +2140,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 41, + funcId: 45, port: port_, ); }, @@ -2051,7 +2173,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 42, + funcId: 46, port: port_, ); }, @@ -2083,7 +2205,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 47, port: port_, ); }, @@ -2113,7 +2235,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 44, + funcId: 48, port: port_, ); }, @@ -2148,7 +2270,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 45, + funcId: 49, port: port_, ); }, @@ -2178,7 +2300,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 50, port: port_, ); }, @@ -2208,7 +2330,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 47, + funcId: 51, port: port_, ); }, @@ -2238,7 +2360,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 48, + funcId: 52, port: port_, ); }, @@ -2279,7 +2401,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 49, + funcId: 53, port: port_, ); }, @@ -2326,7 +2448,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 50, + funcId: 54, port: port_, ); }, @@ -2359,7 +2481,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 51, + funcId: 55, port: port_, ); }, @@ -2392,7 +2514,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 52, + funcId: 56, port: port_, ); }, @@ -2432,7 +2554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 53, + funcId: 57, port: port_, ); }, @@ -2469,7 +2591,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 54, + funcId: 58, port: port_, ); }, @@ -2505,7 +2627,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 55, + funcId: 59, port: port_, ); }, @@ -2540,7 +2662,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 56, + funcId: 60, port: port_, ); }, @@ -2577,7 +2699,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 57, + funcId: 61, port: port_, ); }, @@ -2614,7 +2736,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 58, + funcId: 62, port: port_, ); }, @@ -2644,7 +2766,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 59, + funcId: 63, port: port_, ); }, @@ -2677,7 +2799,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 60, + funcId: 64, port: port_, ); }, @@ -2712,7 +2834,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 61, + funcId: 65, port: port_, ); }, @@ -2742,7 +2864,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 62, + funcId: 66, port: port_, ); }, @@ -2777,7 +2899,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 63, + funcId: 67, port: port_, ); }, @@ -2807,7 +2929,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 64, + funcId: 68, port: port_, ); }, @@ -2839,7 +2961,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 65, + funcId: 69, port: port_, ); }, @@ -2878,7 +3000,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 66, + funcId: 70, port: port_, ); }, @@ -2917,7 +3039,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 67, + funcId: 71, port: port_, ); }, @@ -2959,7 +3081,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 68, + funcId: 72, port: port_, ); }, @@ -2990,7 +3112,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 69, + funcId: 73, port: port_, ); }, @@ -3023,7 +3145,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 70, + funcId: 74, port: port_, ); }, @@ -3056,7 +3178,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 71, + funcId: 75, port: port_, ); }, @@ -3091,7 +3213,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 72, + funcId: 76, port: port_, ); }, @@ -3124,7 +3246,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 73, + funcId: 77, port: port_, ); }, @@ -3157,7 +3279,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 74, + funcId: 78, port: port_, ); }, @@ -3190,7 +3312,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 75, + funcId: 79, port: port_, ); }, @@ -3227,7 +3349,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 76, + funcId: 80, port: port_, ); }, @@ -3257,7 +3379,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 81, port: port_, ); }, @@ -3287,7 +3409,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 78, + funcId: 82, port: port_, ); }, @@ -3317,7 +3439,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 83, port: port_, ); }, @@ -3350,7 +3472,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 84, port: port_, ); }, @@ -3381,7 +3503,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 85, port: port_, ); }, @@ -3414,7 +3536,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 86, port: port_, ); }, @@ -3460,7 +3582,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 83, + funcId: 87, port: port_, ); }, @@ -3516,7 +3638,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 88, port: port_, ); }, @@ -3562,7 +3684,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 89, port: port_, ); }, @@ -3595,7 +3717,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 90, port: port_, ); }, @@ -3630,7 +3752,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 91, port: port_, ); }, @@ -3665,7 +3787,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 92, port: port_, ); }, @@ -3698,7 +3820,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 89, + funcId: 93, port: port_, ); }, @@ -3729,7 +3851,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 90, + funcId: 94, port: port_, ); }, @@ -3767,7 +3889,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 91, + funcId: 95, port: port_, ); }, @@ -3802,7 +3924,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 92, + funcId: 96, port: port_, ); }, @@ -3837,7 +3959,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 93, + funcId: 97, port: port_, ); }, @@ -3868,7 +3990,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 94, + funcId: 98, port: port_, ); }, @@ -3903,7 +4025,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 95, + funcId: 99, port: port_, ); }, @@ -3936,7 +4058,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 96, + funcId: 100, port: port_, ); }, @@ -3971,7 +4093,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 97, + funcId: 101, port: port_, ); }, @@ -4002,7 +4124,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 98, + funcId: 102, port: port_, ); }, @@ -4039,7 +4161,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 99, + funcId: 103, port: port_, ); }, @@ -4091,7 +4213,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 104, port: port_, ); }, @@ -4144,7 +4266,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 105, port: port_, ); }, @@ -4184,7 +4306,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 106, port: port_, ); }, @@ -4217,7 +4339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 107, port: port_, ); }, @@ -4250,7 +4372,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 108, port: port_, ); }, @@ -4287,7 +4409,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 109, port: port_, ); }, @@ -4319,7 +4441,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 110, port: port_, ); }, @@ -4352,7 +4474,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 111, port: port_, ); }, @@ -4387,7 +4509,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 112, port: port_, ); }, @@ -4419,7 +4541,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 113, port: port_, ); }, @@ -4457,7 +4579,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 114, port: port_, ); }, @@ -4490,7 +4612,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 111, + funcId: 115, port: port_, ); }, @@ -4528,7 +4650,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 112, + funcId: 116, port: port_, ); }, @@ -4565,7 +4687,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 113, + funcId: 117, port: port_, ); }, @@ -4602,7 +4724,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 114, + funcId: 118, port: port_, ); }, @@ -4640,7 +4762,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 115, + funcId: 119, port: port_, ); }, @@ -4678,7 +4800,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 116, + funcId: 120, port: port_, ); }, @@ -4711,7 +4833,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 117, + funcId: 121, port: port_, ); }, @@ -4743,7 +4865,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 118, + funcId: 122, port: port_, ); }, @@ -4778,7 +4900,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 119, + funcId: 123, port: port_, ); }, @@ -4815,7 +4937,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 120, + funcId: 124, port: port_, ); }, @@ -4847,7 +4969,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 121, + funcId: 125, port: port_, ); }, @@ -4879,7 +5001,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 122, + funcId: 126, port: port_, ); }, @@ -4914,7 +5036,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 123, + funcId: 127, port: port_, ); }, @@ -4953,7 +5075,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 124, + funcId: 128, port: port_, ); }, @@ -4990,7 +5112,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 125, + funcId: 129, port: port_, ); }, @@ -5020,7 +5142,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 126, + funcId: 130, port: port_, ); }, @@ -5052,7 +5174,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 127, + funcId: 131, port: port_, ); }, @@ -5087,7 +5209,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 128, + funcId: 132, port: port_, ); }, @@ -5119,7 +5241,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 129, + funcId: 133, port: port_, ); }, @@ -5157,7 +5279,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 130, + funcId: 134, port: port_, ); }, @@ -5196,7 +5318,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 131, + funcId: 135, port: port_, ); }, @@ -5231,7 +5353,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 132, + funcId: 136, port: port_, ); }, @@ -5252,37 +5374,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["shares", "threshold"], ); - @override - Future crateBridgeCallbacksLogStripAnsi({required String input}) { - return handler.executeNormal( - NormalTask( - callFfi: (port_) { - final serializer = SseSerializer(generalizedFrbRustBinding); - sse_encode_String(input, serializer); - pdeCallFfi( - generalizedFrbRustBinding, - serializer, - funcId: 133, - port: port_, - ); - }, - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: null, - ), - constMeta: kCrateBridgeCallbacksLogStripAnsiConstMeta, - argValues: [input], - apiImpl: this, - ), - ); - } - - TaskConstMeta get kCrateBridgeCallbacksLogStripAnsiConstMeta => - const TaskConstMeta( - debugName: "strip_ansi", - argNames: ["input"], - ); - @override Future crateBridgeGroupsUpdateChatDeletionTime({ required String groupId, @@ -5297,7 +5388,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 134, + funcId: 137, port: port_, ); }, @@ -5332,7 +5423,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 135, + funcId: 138, port: port_, ); }, @@ -5365,7 +5456,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 136, + funcId: 139, )!; }, codec: SseCodec( @@ -5405,7 +5496,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 137, + funcId: 140, port: port_, ); }, @@ -5450,7 +5541,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 138, + funcId: 141, port: port_, ); }, @@ -5480,7 +5571,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 139, + funcId: 142, port: port_, ); }, @@ -5513,7 +5604,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 140, + funcId: 143, port: port_, ); }, @@ -5548,7 +5639,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 141, + funcId: 144, port: port_, ); }, @@ -5569,43 +5660,43 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["base", "config"], ); - Future Function( - int, - ) - encode_DartFn_Inputs__Output_StreamSink_String_Sse_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_StreamSink_String_Sse(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, - ); - }; + @override + void crateBridgeLoggingWriteLog({ + required LogLevel level, + required String source, + required String message, + required bool inBackground, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_log_level(level, serializer); + sse_encode_String(source, serializer); + sse_encode_String(message, serializer); + sse_encode_bool(inBackground, serializer); + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 145, + )!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateBridgeLoggingWriteLogConstMeta, + argValues: [level, source, message, inBackground], + apiImpl: this, + ), + ); } + TaskConstMeta get kCrateBridgeLoggingWriteLogConstMeta => const TaskConstMeta( + debugName: "write_log", + argNames: ["level", "source", "message", "inBackground"], + ); + Future Function(int, dynamic) encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( FutureOr Function(PlatformInt64) raw, @@ -5688,15 +5779,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dcoDecodeTimestamp(ts: dco_decode_i_64(raw).toInt(), isUtc: true); } - @protected - FutureOr> Function() - dco_decode_DartFn_Inputs__Output_StreamSink_String_Sse_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) { @@ -5741,12 +5823,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - @protected - RustStreamSink dco_decode_StreamSink_String_Sse(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - throw UnimplementedError(); - } - @protected RustStreamSink dco_decode_StreamSink_api_event_Sse(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -6206,6 +6282,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (raw as List).map(dco_decode_sql_value).toList(); } + @protected + LogLevel dco_decode_log_level(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return LogLevel.values[raw as int]; + } + @protected MediaSizeReport dco_decode_media_size_report(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -6560,8 +6642,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 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 != 60) - throw Exception('unexpected arr length: expect 60 but see ${arr.length}'); + if (arr.length != 59) + throw Exception('unexpected arr length: expect 59 but see ${arr.length}'); return UserConfig( userId: dco_decode_i_64(arr[0]), username: dco_decode_String(arr[1]), @@ -6593,41 +6675,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { storeMediaFilesInGallery: dco_decode_bool(arr[27]), autoStoreAllSendUnlimitedMediaFiles: dco_decode_bool(arr[28]), typingIndicators: dco_decode_bool(arr[29]), - sealedSenderEnabled: dco_decode_bool(arr[30]), - showRestoreFlame: dco_decode_bool(arr[31]), - myBestFriendGroupId: dco_decode_opt_String(arr[32]), + showRestoreFlame: dco_decode_bool(arr[30]), + myBestFriendGroupId: dco_decode_opt_String(arr[31]), signalLastSignedPreKeyUpdated: dco_decode_opt_box_autoadd_Chrono_Utc( - arr[33], + arr[32], ), signalLastPqcPreKeysUploaded: dco_decode_opt_box_autoadd_Chrono_Utc( - arr[34], + arr[33], ), - allowErrorTrackingViaSentry: dco_decode_bool(arr[35]), - screenLockEnabled: dco_decode_bool(arr[36]), - isCloudBackupEnabled: dco_decode_bool(arr[37]), - isUserDiscoveryEnabled: dco_decode_bool(arr[38]), - requiredSendImages: dco_decode_i_64(arr[39]), - userDiscoveryThreshold: dco_decode_u_8(arr[40]), - userDiscoveryRequiresManualApproval: dco_decode_bool(arr[41]), - userDiscoverySharePromotion: dco_decode_bool(arr[42]), - userDiscoveryInitializationError: dco_decode_bool(arr[43]), - askForFriendPromotions: dco_decode_bool(arr[44]), - currentPreKeyIndexStart: dco_decode_i_64(arr[45]), - currentSignedPreKeyIndexStart: dco_decode_i_64(arr[46]), - lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[47]), - hideChangeLog: dco_decode_bool(arr[48]), - hideMemoriesBackupPromo: dco_decode_bool(arr[49]), - updateFcmToken: dco_decode_bool(arr[50]), - canUseLoginTokenForAuth: dco_decode_bool(arr[51]), - twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[52]), - isBackupEnabled: dco_decode_bool(arr[53]), + 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[54]), - fcmToken: dco_decode_opt_String(arr[55]), - lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[56]), - currentSetupPage: dco_decode_opt_String(arr[57]), - skipSetupPages: dco_decode_bool(arr[58]), - hasZoomed: dco_decode_bool(arr[59]), + dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[53]), + fcmToken: dco_decode_opt_String(arr[54]), + lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[55]), + currentSetupPage: dco_decode_opt_String(arr[56]), + skipSetupPages: dco_decode_bool(arr[57]), + hasZoomed: dco_decode_bool(arr[58]), ); } @@ -6685,14 +6766,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); } - @protected - RustStreamSink sse_decode_StreamSink_String_Sse( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - throw UnimplementedError('Unreachable ()'); - } - @protected RustStreamSink sse_decode_StreamSink_api_event_Sse( SseDeserializer deserializer, @@ -7259,6 +7332,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + LogLevel sse_decode_log_level(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return LogLevel.values[inner]; + } + @protected MediaSizeReport sse_decode_media_size_report(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -7714,7 +7794,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_storeMediaFilesInGallery = sse_decode_bool(deserializer); var var_autoStoreAllSendUnlimitedMediaFiles = sse_decode_bool(deserializer); var var_typingIndicators = sse_decode_bool(deserializer); - var var_sealedSenderEnabled = sse_decode_bool(deserializer); var var_showRestoreFlame = sse_decode_bool(deserializer); var var_myBestFriendGroupId = sse_decode_opt_String(deserializer); var var_signalLastSignedPreKeyUpdated = @@ -7784,7 +7863,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { autoStoreAllSendUnlimitedMediaFiles: var_autoStoreAllSendUnlimitedMediaFiles, typingIndicators: var_typingIndicators, - sealedSenderEnabled: var_sealedSenderEnabled, showRestoreFlame: var_showRestoreFlame, myBestFriendGroupId: var_myBestFriendGroupId, signalLastSignedPreKeyUpdated: var_signalLastSignedPreKeyUpdated, @@ -7848,18 +7926,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - @protected - void sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException( - FutureOr> Function() self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_DartOpaque( - encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(self), - serializer, - ); - } - @protected void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( FutureOr Function(PlatformInt64) self, @@ -7923,23 +7989,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - @protected - void sse_encode_StreamSink_String_Sse( - RustStreamSink self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_String( - self.setupAndSerialize( - codec: SseCodec( - decodeSuccessData: sse_decode_String, - decodeErrorData: sse_decode_AnyhowException, - ), - ), - serializer, - ); - } - @protected void sse_encode_StreamSink_api_event_Sse( RustStreamSink self, @@ -8473,6 +8522,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_log_level(LogLevel self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + @protected void sse_encode_media_size_report( MediaSizeReport self, @@ -8884,7 +8939,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_bool(self.storeMediaFilesInGallery, serializer); sse_encode_bool(self.autoStoreAllSendUnlimitedMediaFiles, serializer); sse_encode_bool(self.typingIndicators, serializer); - sse_encode_bool(self.sealedSenderEnabled, serializer); sse_encode_bool(self.showRestoreFlame, serializer); sse_encode_opt_String(self.myBestFriendGroupId, serializer); sse_encode_opt_box_autoadd_Chrono_Utc( diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart index 5d64dd44..1d72cf5d 100644 --- a/lib/core/frb_generated.io.dart +++ b/lib/core/frb_generated.io.dart @@ -7,8 +7,8 @@ import 'api/server/prekeys.dart'; import 'bridge.dart'; import 'bridge/api.dart'; import 'bridge/callbacks.dart'; -import 'bridge/callbacks/log.dart'; import 'bridge/groups.dart'; +import 'bridge/logging.dart'; import 'bridge/user_config.dart'; import 'bridge/wrapper.dart'; import 'bridge/wrapper/app_database.dart'; @@ -42,12 +42,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected DateTime dco_decode_Chrono_Utc(dynamic raw); - @protected - FutureOr> Function() - dco_decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException( - dynamic raw, - ); - @protected FutureOr Function(PlatformInt64) dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw); @@ -65,9 +59,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Map> dco_decode_Map_String_list_String_None(dynamic raw); - @protected - RustStreamSink dco_decode_StreamSink_String_Sse(dynamic raw); - @protected RustStreamSink dco_decode_StreamSink_api_event_Sse(dynamic raw); @@ -243,6 +234,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_sql_value(dynamic raw); + @protected + LogLevel dco_decode_log_level(dynamic raw); + @protected MediaSizeReport dco_decode_media_size_report(dynamic raw); @@ -397,11 +391,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); - @protected - RustStreamSink sse_decode_StreamSink_String_Sse( - SseDeserializer deserializer, - ); - @protected RustStreamSink sse_decode_StreamSink_api_event_Sse( SseDeserializer deserializer, @@ -617,6 +606,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List sse_decode_list_sql_value(SseDeserializer deserializer); + @protected + LogLevel sse_decode_log_level(SseDeserializer deserializer); + @protected MediaSizeReport sse_decode_media_size_report(SseDeserializer deserializer); @@ -779,12 +771,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer); - @protected - void sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException( - FutureOr> Function() self, - SseSerializer serializer, - ); - @protected void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( FutureOr Function(PlatformInt64) self, @@ -812,12 +798,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_StreamSink_String_Sse( - RustStreamSink self, - SseSerializer serializer, - ); - @protected void sse_encode_StreamSink_api_event_Sse( RustStreamSink self, @@ -1088,6 +1068,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_list_sql_value(List self, SseSerializer serializer); + @protected + void sse_encode_log_level(LogLevel self, SseSerializer serializer); + @protected void sse_encode_media_size_report( MediaSizeReport self, diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart index 7b54429d..1f9d6bcb 100644 --- a/lib/core/frb_generated.web.dart +++ b/lib/core/frb_generated.web.dart @@ -10,8 +10,8 @@ import 'api/server/prekeys.dart'; import 'bridge.dart'; import 'bridge/api.dart'; import 'bridge/callbacks.dart'; -import 'bridge/callbacks/log.dart'; import 'bridge/groups.dart'; +import 'bridge/logging.dart'; import 'bridge/user_config.dart'; import 'bridge/wrapper.dart'; import 'bridge/wrapper/app_database.dart'; @@ -44,12 +44,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected DateTime dco_decode_Chrono_Utc(dynamic raw); - @protected - FutureOr> Function() - dco_decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException( - dynamic raw, - ); - @protected FutureOr Function(PlatformInt64) dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw); @@ -67,9 +61,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Map> dco_decode_Map_String_list_String_None(dynamic raw); - @protected - RustStreamSink dco_decode_StreamSink_String_Sse(dynamic raw); - @protected RustStreamSink dco_decode_StreamSink_api_event_Sse(dynamic raw); @@ -245,6 +236,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_sql_value(dynamic raw); + @protected + LogLevel dco_decode_log_level(dynamic raw); + @protected MediaSizeReport dco_decode_media_size_report(dynamic raw); @@ -399,11 +393,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); - @protected - RustStreamSink sse_decode_StreamSink_String_Sse( - SseDeserializer deserializer, - ); - @protected RustStreamSink sse_decode_StreamSink_api_event_Sse( SseDeserializer deserializer, @@ -619,6 +608,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List sse_decode_list_sql_value(SseDeserializer deserializer); + @protected + LogLevel sse_decode_log_level(SseDeserializer deserializer); + @protected MediaSizeReport sse_decode_media_size_report(SseDeserializer deserializer); @@ -781,12 +773,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer); - @protected - void sse_encode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException( - FutureOr> Function() self, - SseSerializer serializer, - ); - @protected void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( FutureOr Function(PlatformInt64) self, @@ -814,12 +800,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_StreamSink_String_Sse( - RustStreamSink self, - SseSerializer serializer, - ); - @protected void sse_encode_StreamSink_api_event_Sse( RustStreamSink self, @@ -1090,6 +1070,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_list_sql_value(List self, SseSerializer serializer); + @protected + void sse_encode_log_level(LogLevel self, SseSerializer serializer); + @protected void sse_encode_media_size_report( MediaSizeReport self, diff --git a/lib/core/user_config.dart b/lib/core/user_config.dart index c03cc3ac..d3fd8192 100644 --- a/lib/core/user_config.dart +++ b/lib/core/user_config.dart @@ -133,11 +133,6 @@ class UserConfig { bool storeMediaFilesInGallery; bool autoStoreAllSendUnlimitedMediaFiles; bool typingIndicators; - - /// Announces to contacts that this account accepts sealed-sender envelopes, - /// and lets this account send them. Both sides have to have it on before - /// anything travels sealed. - bool sealedSenderEnabled; bool showRestoreFlame; String? myBestFriendGroupId; DateTime? signalLastSignedPreKeyUpdated; @@ -203,7 +198,6 @@ class UserConfig { required this.storeMediaFilesInGallery, required this.autoStoreAllSendUnlimitedMediaFiles, required this.typingIndicators, - required this.sealedSenderEnabled, required this.showRestoreFlame, this.myBestFriendGroupId, this.signalLastSignedPreKeyUpdated, @@ -267,7 +261,6 @@ class UserConfig { storeMediaFilesInGallery.hashCode ^ autoStoreAllSendUnlimitedMediaFiles.hashCode ^ typingIndicators.hashCode ^ - sealedSenderEnabled.hashCode ^ showRestoreFlame.hashCode ^ myBestFriendGroupId.hashCode ^ signalLastSignedPreKeyUpdated.hashCode ^ @@ -336,7 +329,6 @@ class UserConfig { autoStoreAllSendUnlimitedMediaFiles == other.autoStoreAllSendUnlimitedMediaFiles && typingIndicators == other.typingIndicators && - sealedSenderEnabled == other.sealedSenderEnabled && showRestoreFlame == other.showRestoreFlame && myBestFriendGroupId == other.myBestFriendGroupId && signalLastSignedPreKeyUpdated == diff --git a/lib/main.dart b/lib/main.dart index 8f0fc4ec..39bd2259 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -55,6 +55,7 @@ Future twonlyMinimumInitialization() async { dataDir: AppEnvironment.supportDir, ), ); + Log.enableRustSink(); if (!await RustAppDatabase.legacyImportComplete()) { final legacyFile = File( '${AppEnvironment.supportDir}/twonly.sqlite', @@ -71,6 +72,9 @@ Future twonlyMinimumInitialization() async { await RustAppDatabase.migrateLegacyDatabase(); } } catch (e) { + // Tracing is initialized before the rest of the Rust context, so even + // failed initialization can persist the buffered startup diagnostics. + Log.enableRustSink(); Log.error(e); return true; } @@ -188,7 +192,10 @@ Future postStartupTasks() async { unawaited(MediaFileService.purgeTempFolder()); // 2. Service initializations - unawaited(RustApi.finishStartedMediaUploads()); + unawaitedRustCall( + RustApi.finishStartedMediaUploads(), + 'finishStartedMediaUploads', + ); unawaited( newsService.init().then((_) { final lastDownload = newsService.lastDownloadedAt; diff --git a/lib/src/callbacks/callbacks.dart b/lib/src/callbacks/callbacks.dart index 59f25b32..5c7b906a 100644 --- a/lib/src/callbacks/callbacks.dart +++ b/lib/src/callbacks/callbacks.dart @@ -1,13 +1,11 @@ import 'package:twonly/core/bridge/callbacks.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/callbacks/logging.callbacks.dart'; import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/services/user.service.dart'; Future initFlutterCallbacksForRust() async { await initFlutterCallbacks( callbackId: isolateCallbackId, - loggingGetStreamSink: LoggingCallbacks.getStreamSink, apiVerificationSucceeded: KeyVerificationService.handleVerificationSucceeded, apiUserConfigChanged: UserService.handleRustUserConfigChanged, diff --git a/lib/src/callbacks/logging.callbacks.dart b/lib/src/callbacks/logging.callbacks.dart deleted file mode 100644 index 8f5a3c40..00000000 --- a/lib/src/callbacks/logging.callbacks.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -import 'package:logging/logging.dart'; -import 'package:twonly/src/utils/log.dart'; - -/// Matches ANSI escape sequences (CSI sequences, caret notation like ^[[3m or \^[[3m). -final _ansiRegex = RegExp(r'(?:\x1B|\\?\^\[)\[[0-?]*[ -/]*[@-~]'); - -/// Matches the plain `ShortEventFormatter` output from `rust/src/log.rs`: -/// `HH:MM:SS LEVEL dir/file.rs:12 `. -final _rustLogLine = RegExp( - r'^\d{2}:\d{2}:\d{2} (TRACE|DEBUG|INFO|WARN|ERROR) +(\S+:\d+) ?(.*)$', - dotAll: true, -); - -/// Rust levels mapped onto the `logging` levels the Dart side already prints. -/// `SHOUT` is what [Log.error] uses, so a Rust `ERROR` reads the same as a Dart -/// one. -const Map _levels = { - 'TRACE': Level.FINEST, - 'DEBUG': Level.FINE, - 'INFO': Level.INFO, - 'WARN': Level.WARNING, - 'ERROR': Level.SHOUT, -}; - -class LoggingCallbacks { - static Future> getStreamSink() async { - final dartLogSink = RustStreamSink(); - - // `stream` throws until flutter_rust_bridge has serialized the sink for - // Rust, which only happens once this function has returned. Poll for it - // instead of racing; buffered events are replayed on the first listen. - var attempts = 0; - Timer.periodic(const Duration(milliseconds: 100), (timer) { - attempts++; - try { - dartLogSink.stream.listen(_handleRustLog); - timer.cancel(); - } catch (_) { - // Stream not yet initialized. - if (attempts >= 100) { - timer.cancel(); - Log.warn('Rust log sink never became available.'); - } - } - }); - - return dartLogSink; - } - - @visibleForTesting - static void handleRustLog(String log) => _handleRustLog(log); - - static void _handleRustLog(String log) { - final sanitizedLog = log.replaceAll(_ansiRegex, ''); - final match = _rustLogLine.firstMatch(sanitizedLog); - if (match == null) { - // Not a formatted event (panic output, a continuation line, ...). - Log.warn(sanitizedLog); - return; - } - - // The level and the `file.rs:line` origin belong in the record itself, not - // repeated inside the message: the Dart call site here says nothing useful. - Log.forward( - level: _levels[match.group(1)] ?? Level.INFO, - source: match.group(2)!, - messageInput: match.group(3), - ); - } -} diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 52840ae5..9729bcca 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -56,10 +56,6 @@ enum MessageActionType { openedAt, ackByUserAt, ackByServerAt, - - /// Present when this recipient's copy left as a sealed-sender envelope. The - /// transport is per recipient, so in a group only some members may have one. - sealedSenderAt, } @DataClassName('MessageAction') diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 14031316..7abe3f11 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1832,36 +1832,18 @@ abstract class AppLocalizations { /// **'Waiting for internet'** String get waitingForInternet; - /// No description provided for @sealedSenderTransportSealed. - /// - /// In en, this message translates to: - /// **'Sealed sender'** - String get sealedSenderTransportSealed; - - /// No description provided for @sealedSenderTransportStandard. - /// - /// In en, this message translates to: - /// **'Standard'** - String get sealedSenderTransportStandard; - - /// No description provided for @settingsSealedSender. - /// - /// In en, this message translates to: - /// **'Sealed Sender'** - String get settingsSealedSender; - - /// No description provided for @settingsSealedSenderSubtitle. - /// - /// In en, this message translates to: - /// **'Hides who you are from the server when sending. Both you and your contact need it turned on, so turning it off also stops your contacts from sending to you sealed.'** - String get settingsSealedSenderSubtitle; - /// No description provided for @editHistory. /// /// In en, this message translates to: /// **'Edit history'** String get editHistory; + /// No description provided for @fileSize. + /// + /// In en, this message translates to: + /// **'File size'** + String get fileSize; + /// No description provided for @archivedChats. /// /// In en, this message translates to: @@ -2252,6 +2234,12 @@ abstract class AppLocalizations { /// **'Messages from other users.'** String get notificationCategoryMessageDesc; + /// No description provided for @notificationConnectionFallback. + /// + /// In en, this message translates to: + /// **'You may have new messages.'** + String get notificationConnectionFallback; + /// No description provided for @groupContextMenuDeleteGroup. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 9bf46143..095ab75d 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -980,22 +980,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get waitingForInternet => 'Warten auf Internet'; - @override - String get sealedSenderTransportSealed => 'Sealed Sender'; - - @override - String get sealedSenderTransportStandard => 'Standard'; - - @override - String get settingsSealedSender => 'Sealed Sender'; - - @override - String get settingsSealedSenderSubtitle => - 'Verbirgt beim Senden vor dem Server, wer du bist. Sowohl du als auch dein Kontakt müssen es aktiviert haben; deaktivierst du es, senden deine Kontakte auch nicht mehr versiegelt an dich.'; - @override String get editHistory => 'Bearbeitungshistorie'; + @override + String get fileSize => 'Dateigröße'; + @override String get archivedChats => 'Archivierte Chats'; @@ -1254,6 +1244,10 @@ class AppLocalizationsDe extends AppLocalizations { String get notificationCategoryMessageDesc => 'Nachrichten von anderen Benutzern.'; + @override + String get notificationConnectionFallback => + 'Du könntest neue Nachrichten haben.'; + @override String get groupContextMenuDeleteGroup => 'Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 7f106c60..d7936b11 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -973,22 +973,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get waitingForInternet => 'Waiting for internet'; - @override - String get sealedSenderTransportSealed => 'Sealed sender'; - - @override - String get sealedSenderTransportStandard => 'Standard'; - - @override - String get settingsSealedSender => 'Sealed Sender'; - - @override - String get settingsSealedSenderSubtitle => - 'Hides who you are from the server when sending. Both you and your contact need it turned on, so turning it off also stops your contacts from sending to you sealed.'; - @override String get editHistory => 'Edit history'; + @override + String get fileSize => 'File size'; + @override String get archivedChats => 'Archived chats'; @@ -1246,6 +1236,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get notificationCategoryMessageDesc => 'Messages from other users.'; + @override + String get notificationConnectionFallback => 'You may have new messages.'; + @override String get groupContextMenuDeleteGroup => 'This will permanently delete all messages in this chat.'; diff --git a/lib/src/services/api/api.service.dart b/lib/src/services/api/api.service.dart index 01ef09ff..90a3a1bb 100644 --- a/lib/src/services/api/api.service.dart +++ b/lib/src/services/api/api.service.dart @@ -42,7 +42,10 @@ class ApiService { if (AppState.isInBackgroundTask) { await rust_api.RustApi.reuploadPendingMedia(); } else if (!AppState.isAppInBackground) { - unawaited(rust_api.RustApi.reuploadPendingMedia()); + unawaitedRustCall( + rust_api.RustApi.reuploadPendingMedia(), + 'reuploadPendingMedia', + ); twonlyDB.markUpdated(); // resetUserDiscoveryRequestUpdates(); diff --git a/lib/src/services/api/rust_api_result.dart b/lib/src/services/api/rust_api_result.dart index 05787e28..a8949e8b 100644 --- a/lib/src/services/api/rust_api_result.dart +++ b/lib/src/services/api/rust_api_result.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:twonly/src/model/error_code.dart'; import 'package:twonly/src/utils/log.dart'; @@ -27,3 +29,18 @@ Future> rustApiResult(Future request) async { return Result.error(ErrorCode.InternalError); } } + +/// Starts a Rust API call that nothing waits on. +/// +/// Anything crossing the bridge can fail on transport alone — the WebSocket is +/// still connecting at startup, or it drops mid-request — and a rejected future +/// with no listener surfaces as an unhandled exception in the root zone. These +/// calls are all retried by Rust or repeated by the next tick, so the failure +/// only has to be logged. +void unawaitedRustCall(Future request, String description) { + unawaited( + request.catchError((Object error) { + Log.warn('$description failed', error); + }), + ); +} diff --git a/lib/src/services/migrations.service.dart b/lib/src/services/migrations.service.dart index 7779bae4..c04fe5f6 100644 --- a/lib/src/services/migrations.service.dart +++ b/lib/src/services/migrations.service.dart @@ -108,10 +108,6 @@ Future runMigrations() async { await UserService.update((u) => u.appVersion = 118); } - // The server only delivers sealed-sender payloads to clients reporting at - // least this version, so the bump is what announces support for them. The - // `sealedSenderEnabled` setting itself needs no migration: an existing - // user.json without the key falls back to the enabled default. if (userService.currentUser.appVersion < 119) { await UserService.update((u) => u.appVersion = 119); } diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index 16e8f267..78fc8cf1 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -1,22 +1,29 @@ -import 'dart:async'; import 'dart:io'; -import 'package:clock/clock.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; -import 'package:mutex/mutex.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:twonly/core/bridge/logging.dart' as rust_logging; import 'package:twonly/globals.dart'; -import 'package:twonly/src/utils/exclusive_access.utils.dart'; class Log { static bool _isInitialized = false; + static bool _rustSinkReady = false; + static const int _maxBufferedRecords = 1000; + static final List _bufferedRecords = []; static void init() { if (_isInitialized) return; _isInitialized = true; Logger.root.level = Level.ALL; - Logger.root.onRecord.listen((record) async { - unawaited(_writeLogToFile(record)); + Logger.root.onRecord.listen((record) { + if (_rustSinkReady) { + if (!_writeLogToRust(record)) { + _rustSinkReady = false; + _buffer(record); + } + } else { + _buffer(record); + } if (!kReleaseMode) { if (!Platform.environment.containsKey('FLUTTER_TEST') || record.level >= Level.WARNING) { @@ -29,6 +36,54 @@ class Log { }); } + /// Enables the Rust-owned app.log and flushes records emitted before Rust + /// initialization completed. + static void enableRustSink() { + if (_rustSinkReady) return; + _rustSinkReady = true; + final pending = List.of(_bufferedRecords); + _bufferedRecords.clear(); + for (var i = 0; i < pending.length; i++) { + if (_writeLogToRust(pending[i])) continue; + _rustSinkReady = false; + for (var j = i; j < pending.length; j++) { + _buffer(pending[j]); + } + break; + } + } + + static void _buffer(LogRecord record) { + if (_bufferedRecords.length == _maxBufferedRecords) { + _bufferedRecords.removeAt(0); + } + _bufferedRecords.add(record); + } + + static bool _writeLogToRust(LogRecord record) { + try { + rust_logging.writeLog( + level: switch (record.level) { + >= Level.SHOUT => rust_logging.LogLevel.shout, + >= Level.WARNING => rust_logging.LogLevel.warning, + >= Level.INFO => rust_logging.LogLevel.info, + >= Level.FINE => rust_logging.LogLevel.fine, + _ => rust_logging.LogLevel.finest, + }, + source: record.loggerName, + message: record.message, + inBackground: AppState.isInBackgroundTask, + ); + return true; + } catch (error) { + if (!kReleaseMode) { + // ignore: avoid_print + print('Could not forward log record to Rust: $error'); + } + return false; + } + } + static String filterLogMessage(String msg) { if (msg.contains('SqliteException')) { // Do not log data which would be inserted into the DB. @@ -77,119 +132,22 @@ class Log { final message = filterLogMessage('$messageInput'); Logger(_getCallerSourceCodeFilename()).fine(message, error, stackTrace); } - - /// Re-emits a record that was produced outside Dart, keeping the origin's - /// level and source location. Deriving either from the Dart call site would - /// only ever point back at the forwarding code. - static void forward({ - required Level level, - required String source, - required Object? messageInput, - }) { - Logger(source).log(level, filterLogMessage('$messageInput')); - } } Future loadLogFile() async { - return _protectFileAccess(() async { - final logFile = File('${AppEnvironment.supportDir}/app.log'); - - if (logFile.existsSync()) { - return logFile.readAsString(); - } else { - return 'Log file does not exist.'; - } - }); + return rust_logging.loadLogFile(); } Future readLast1000Lines() async { - return _protectFileAccess(() async { - final file = File('${AppEnvironment.supportDir}/app.log'); - if (!file.existsSync()) return ''; - final all = await file.readAsLines(); - final start = all.length > 1000 ? all.length - 1000 : 0; - return all.sublist(start).join('\n'); - }); -} - -final Mutex _logMutex = Mutex(); - -Future _protectFileAccess(Future Function() action) async { - return exclusiveAccess( - lockName: 'app.log', - action: action, - mutex: _logMutex, - ); -} - -Future _writeLogToFile(LogRecord record) async { - final logFile = File('${AppEnvironment.supportDir}/app.log'); - - final logMessage = - '${clock.now()} ${record.level.name} [${AppState.isInBackgroundTask ? 'b' : 'f'}] [twonly] ${record.loggerName} > ${record.message}\n'; - - return _protectFileAccess(() async { - if (!logFile.existsSync()) { - logFile.createSync(recursive: true); - } - final raf = await logFile.open(mode: FileMode.writeOnlyAppend); - try { - await raf.writeString(logMessage); - await raf.flush(); - } catch (e) { - // ignore: avoid_print - print('Error during file access: $e'); - } finally { - await raf.close(); - } - }); + return rust_logging.readLastLogLines(lineCount: 1000); } Future cleanLogFile() async { - return _protectFileAccess(() async { - final logFile = File('${AppEnvironment.supportDir}/app.log'); - - if (!logFile.existsSync()) { - return; - } - final lines = await logFile.readAsLines(); - - final twoWeekAgo = clock.now().subtract(const Duration(days: 3)); - var keepStartIndex = -1; - - for (var i = 0; i < lines.length; i += 100) { - if (lines[i].length >= 19) { - final date = DateTime.tryParse(lines[i].substring(0, 19)); - if (date != null && date.isAfter(twoWeekAgo)) { - keepStartIndex = i; - break; - } - } - } - - if (keepStartIndex == 0) return; - - if (keepStartIndex == -1) { - await logFile.writeAsString(''); - return; - } - - final remaining = lines.sublist(keepStartIndex); - final sink = logFile.openWrite()..writeAll(remaining, '\n'); - await sink.close(); - }); + return rust_logging.cleanLogFile(); } Future deleteLogFile() async { - return _protectFileAccess(() async { - final logFile = File('${AppEnvironment.supportDir}/app.log'); - - if (logFile.existsSync()) { - await logFile.delete(); - return true; - } - return false; - }); + return rust_logging.clearLogFile(); } String _getCallerSourceCodeFilename() { diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index 0f61e7be..7f6b56bb 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -249,8 +249,20 @@ class MainCameraController { } if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) { + // Stabilization buys its steadiness with a margin of sensor pixels to + // warp against, so it always narrows the field of view. On Android + // level2 maps to Camera2's PREVIEW_STABILIZATION, which narrows the + // preview by the same amount as the recording, so the viewfinder + // shows what gets sent; level1 is plain VIDEO_STABILIZATION_MODE_ON, + // which crops the recording alone and leaves the preview wider than + // the result. On iOS level2 is AVFoundation's `.cinematic`, which + // crops harder than the `.standard` level1 maps to without matching + // the preview any better, so iOS stays where it was. Both fall back + // down the levels, and finally to off, on a camera without the mode. await controller.setVideoStabilizationMode( - VideoStabilizationMode.level1, + Platform.isAndroid + ? VideoStabilizationMode.level2 + : VideoStabilizationMode.level1, ); if (sessionId != _cameraSessionId) { unawaited(controller.dispose()); diff --git a/lib/src/visual/views/camera/share_image_contact_selection.view.dart b/lib/src/visual/views/camera/share_image_contact_selection.view.dart index f030a353..3da9ceee 100644 --- a/lib/src/visual/views/camera/share_image_contact_selection.view.dart +++ b/lib/src/visual/views/camera/share_image_contact_selection.view.dart @@ -309,7 +309,7 @@ class _ShareImageView extends State { }); // in case mediaStoreFutureReady is ready, the image is stored in the originalPath - unawaited( + unawaitedRustCall( RustApi.sendMediaToGroups( mediaId: widget.mediaFileService.mediaFile.mediaId, @@ -317,6 +317,7 @@ class _ShareImageView extends State { additionalMessageData: widget.additionalData ?.writeToBuffer(), ), + 'sendMediaToGroups', ); if (context.mounted) { diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart index d7338361..4335679e 100644 --- a/lib/src/visual/views/chats/chat_messages.view.dart +++ b/lib/src/visual/views/chats/chat_messages.view.dart @@ -196,13 +196,19 @@ class _ChatMessagesViewState extends State } if (userService.currentUser.typingIndicators) { - unawaited(RustApi.sendTyping(groupId: widget.groupId, isTyping: false)); - _nextTypingIndicator = Timer.periodic(chatOpenPingInterval, (_) async { + unawaitedRustCall( + RustApi.sendTyping(groupId: widget.groupId, isTyping: false), + 'sendTyping', + ); + _nextTypingIndicator = Timer.periodic(chatOpenPingInterval, (_) { // A typing announcement refreshes the contact's chat-open state as // well, so pinging while the composer is active would spend a second // message only to clear the typing flag that composer just set. if (_isViewActive() && !_composing.value) { - await RustApi.sendTyping(groupId: widget.groupId, isTyping: false); + unawaitedRustCall( + RustApi.sendTyping(groupId: widget.groupId, isTyping: false), + 'sendTyping', + ); } }); } diff --git a/lib/src/visual/views/chats/chat_messages_components/message_input.dart b/lib/src/visual/views/chats/chat_messages_components/message_input.dart index 5144dca8..6a000553 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_input.dart @@ -96,13 +96,16 @@ class _MessageInputState extends State { } widget.textFieldFocus.addListener(_handleTextFocusChange); if (userService.currentUser.typingIndicators) { - _nextTypingIndicator = Timer.periodic(typingIndicatorInterval, (_) async { + _nextTypingIndicator = Timer.periodic(typingIndicatorInterval, (_) { final composing = _isComposing; widget.composing.value = composing; if (composing) { - await RustApi.sendTyping( - groupId: widget.group.groupId, - isTyping: true, + unawaitedRustCall( + RustApi.sendTyping( + groupId: widget.group.groupId, + isTyping: true, + ), + 'sendTyping', ); } }); @@ -161,8 +164,9 @@ class _MessageInputState extends State { userService.currentUser.typingIndicators && widget.textFieldFocus.hasFocus) { widget.composing.value = true; - unawaited( + unawaitedRustCall( RustApi.sendTyping(groupId: widget.group.groupId, isTyping: true), + 'sendTyping', ); } } diff --git a/lib/src/visual/views/chats/chat_messages_components/typing_indicator.dart b/lib/src/visual/views/chats/chat_messages_components/typing_indicator.dart index 86178ffe..925b5648 100644 --- a/lib/src/visual/views/chats/chat_messages_components/typing_indicator.dart +++ b/lib/src/visual/views/chats/chat_messages_components/typing_indicator.dart @@ -11,10 +11,7 @@ import 'package:twonly/src/visual/views/chats/chat_messages.view.dart'; /// How often a composing user re-announces that it is typing. /// -/// Every announcement is a message, and a message sent sealed costs a Privacy -/// Pass token out of a daily quota. At the one-second cadence this used to run -/// at, a few minutes of typing spent a whole day's worth, after which every -/// later message — real ones included — fell back to the named transport. +/// Keep presence traffic modest while still making the indicator feel live. const typingIndicatorInterval = Duration(seconds: 4); /// How long a received typing announcement counts for. Comfortably longer than diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index 762a8a43..55fd6452 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -357,8 +357,9 @@ class _MediaViewerViewState extends State { Log.info( 'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}', ); - unawaited( + unawaitedRustCall( RustApi.downloadDone(token: currentMediaLocal.mediaFile.downloadToken!), + 'downloadDone', ); if (currentMediaLocal.mediaFile.type == MediaType.video) { @@ -614,12 +615,13 @@ class _MediaViewerViewState extends State { void _sendTextMessage() { if (textMessageController.text.isNotEmpty) { - unawaited( + unawaitedRustCall( RustApi.insertAndSendText( groupId: widget.group.groupId, text: textMessageController.text, quoteMessageId: currentMessage!.messageId, ), + 'insertAndSendText', ); textMessageController.clear(); } diff --git a/lib/src/visual/views/chats/message_info.view.dart b/lib/src/visual/views/chats/message_info.view.dart index 4ddea6f7..75574e8a 100644 --- a/lib/src/visual/views/chats/message_info.view.dart +++ b/lib/src/visual/views/chats/message_info.view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -9,6 +10,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; @@ -35,10 +37,13 @@ class _MessageInfoViewState extends State { StreamSubscription>? actionsStream; StreamSubscription>? historyStream; StreamSubscription>? groupMemberStream; + StreamSubscription? mediaFileStream; List<(MessageAction, Contact)> messageActions = []; List messageHistory = []; List<(GroupMember, Contact)> groupMembers = []; + MediaFile? mediaFile; + int? mediaSizeInBytes; @override void initState() { @@ -51,6 +56,7 @@ class _MessageInfoViewState extends State { actionsStream?.cancel(); historyStream?.cancel(); groupMemberStream?.cancel(); + mediaFileStream?.cancel(); super.dispose(); } @@ -84,6 +90,35 @@ class _MessageInfoViewState extends State { messageHistory = update; }); }); + + final mediaId = widget.message.mediaId; + if (mediaId != null) { + final streamMedia = twonlyDB.mediaFilesDao.watchMedia(mediaId); + mediaFileStream = streamMedia.listen((update) { + if (!mounted) return; + setState(() { + mediaFile = update; + mediaSizeInBytes = update == null ? null : mediaSize(update); + }); + }); + } + } + + /// Rust records the size when it writes the plaintext, but media that was + /// sent or received before it did so only carries a size once it was stored, + /// so a copy that is still on disk is measured directly. + int? mediaSize(MediaFile media) { + if (media.sizeInBytes != null) return media.sizeInBytes; + final service = MediaFileService(media); + for (final file in [ + service.storedPath, + service.tempPath, + service.originalPath, + ]) { + final stat = file.statSync(); + if (stat.type == FileSystemEntityType.file) return stat.size; + } + return null; } List getReceivedColumns(BuildContext context) { @@ -113,12 +148,6 @@ class _MessageInfoViewState extends State { t.$1.type == MessageActionType.openedAt && t.$2.userId == groupMember.$2.userId, ); - final sealedSender = messageActions.firstWhereOrNull( - (t) => - t.$1.type == MessageActionType.sealedSenderAt && - t.$2.userId == groupMember.$2.userId, - ); - var actionTypeText = context.lang.waitingForInternet; var actionAt = widget.message.createdAt; if (ackByServer != null) { @@ -165,23 +194,6 @@ class _MessageInfoViewState extends State { Text(actionTypeText), ], ), - // The transport is decided per recipient, so it is only known - // once this member's copy has actually left the device. - if (ackByServer != null) ...[ - const SizedBox(width: 10), - Tooltip( - message: sealedSender != null - ? context.lang.sealedSenderTransportSealed - : context.lang.sealedSenderTransportStandard, - child: FaIcon( - sealedSender != null - ? FontAwesomeIcons.solidEnvelope - : FontAwesomeIcons.envelope, - size: 13, - color: Theme.of(context).hintColor, - ), - ), - ], ], ), ), @@ -228,6 +240,10 @@ class _MessageInfoViewState extends State { Text( '${context.lang.received}: ${friendlyDateTime(context, widget.message.ackByServer!)}', ), + if (mediaSizeInBytes != null) + Text( + '${context.lang.fileSize}: ${formatBytes(mediaSizeInBytes!)}', + ), if (userService.currentUser.isDeveloper) GestureDetector( onTap: () async { 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 85cf881f..07e0d6f2 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,10 @@ class _StorageContentsViewState extends State { try { if (deleteCompletely) { await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); - unawaited(RustApi.deleteMemory(mediaId: file.mediaId)); + unawaitedRustCall( + RustApi.deleteMemory(mediaId: file.mediaId), + 'deleteMemory', + ); await MediaFileService(file).fullMediaRemoval(); } else { MediaFileService(file).storedPath.deleteSync(); @@ -384,7 +387,10 @@ class _StorageContentsViewState extends State { for (final file in selectedFiles) { if (deleteCompletely) { await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); - unawaited(RustApi.deleteMemory(mediaId: file.mediaId)); + unawaitedRustCall( + RustApi.deleteMemory(mediaId: file.mediaId), + 'deleteMemory', + ); await MediaFileService(file).fullMediaRemoval(); } else { MediaFileService(file).storedPath.deleteSync(); diff --git a/lib/src/visual/views/settings/privacy.view.dart b/lib/src/visual/views/settings/privacy.view.dart index e640ac2e..252dc3b7 100644 --- a/lib/src/visual/views/settings/privacy.view.dart +++ b/lib/src/visual/views/settings/privacy.view.dart @@ -40,13 +40,6 @@ class _PrivacyViewState extends State { setState(() {}); } - Future toggleSealedSender() async { - await UserService.update((u) { - u.sealedSenderEnabled = !u.sealedSenderEnabled; - }); - setState(() {}); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -100,16 +93,6 @@ class _PrivacyViewState extends State { ), ), const Divider(), - ListTile( - title: Text(context.lang.settingsSealedSender), - subtitle: Text(context.lang.settingsSealedSenderSubtitle), - onTap: toggleSealedSender, - trailing: Switch.adaptive( - value: userService.currentUser.sealedSenderEnabled, - onChanged: (a) => toggleSealedSender(), - ), - ), - const Divider(), ListTile( title: Text(context.lang.settingsScreenLock), subtitle: Text(context.lang.settingsScreenLockSubtitle), diff --git a/pubspec.lock b/pubspec.lock index f0db06dc..d89953aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -186,10 +186,10 @@ packages: dependency: "direct main" description: name: camera - sha256: "034c38cb8014d29698dcae6d20276688a1bf74e6487dfeb274d70ea05d5f7777" + sha256: "558230d6ce6ccea856b32d390db7e7b557adf4d9320aa614481bd3f2f608953f" url: "https://pub.dev" source: hosted - version: "0.12.0+1" + version: "0.12.0+2" camera_android_camerax: dependency: "direct main" description: diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0e3cd50f..8ce53916 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common 0.1.7", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -256,30 +256,12 @@ dependencies = [ "windows-link", ] -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base16ct" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - [[package]] name = "bindgen" version = "0.72.1" @@ -326,33 +308,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "blind-rsa-signatures" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c8e1ec3966bafbe115ad484420b260f5fabf88528e4ef8cd3024ffedb50e46" -dependencies = [ - "crypto-bigint 0.7.5", - "crypto-primes", - "ct-codecs", - "derive-new", - "derive_more", - "digest 0.11.3", - "hmac-sha256", - "hmac-sha512", - "rand 0.10.2", - "rand_core 0.10.1", - "rsa", - "serde", -] - [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -466,7 +428,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] @@ -605,12 +567,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-oid" version = "0.10.2" @@ -623,15 +579,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -733,15 +680,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" -[[package]] -name = "crossbeam-channel" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -776,39 +714,13 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array 0.14.7", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-bigint" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" -dependencies = [ - "cpubits", - "ctutils", - "num-traits", - "rand_core 0.10.1", - "serdect", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", "typenum", ] @@ -823,23 +735,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "crypto-primes" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a" -dependencies = [ - "crypto-bigint 0.7.5", - "libm", - "rand_core 0.10.1", -] - -[[package]] -name = "ct-codecs" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fb0c6640b4507ebd99ff67677009e381ba5eee1d14df78de4a3d16eb123c39" - [[package]] name = "ctr" version = "0.9.2" @@ -867,23 +762,6 @@ dependencies = [ "cmov", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "fiat-crypto 0.2.9", - "rand_core 0.6.4", - "rustc_version", - "serde", - "subtle", - "zeroize", -] - [[package]] name = "curve25519-dalek" version = "5.0.0" @@ -894,7 +772,7 @@ dependencies = [ "cpufeatures 0.3.0", "curve25519-dalek-derive", "digest 0.11.3", - "fiat-crypto 0.3.0", + "fiat-crypto", "rand_core 0.10.1", "rustc_version", "subtle", @@ -1011,44 +889,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "zeroize", -] - -[[package]] -name = "der" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" -dependencies = [ - "const-oid 0.10.2", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - -[[package]] -name = "derive-new" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "derive-where" version = "1.6.1" @@ -1086,12 +926,10 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", "proc-macro2", "quote", "rustc_version", "syn 2.0.119", - "unicode-xid", ] [[package]] @@ -1112,7 +950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid 0.10.2", + "const-oid", "crypto-common 0.2.2", "ctutils", ] @@ -1149,24 +987,6 @@ dependencies = [ "serde", ] -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", - "digest 0.10.7", - "ff", - "generic-array 0.14.7", - "group", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - [[package]] name = "embedded-io" version = "0.4.0" @@ -1273,22 +1093,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - [[package]] name = "fiat-crypto" version = "0.3.0" @@ -1532,18 +1336,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", -] - -[[package]] -name = "generic-array" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" -dependencies = [ - "rustversion", - "serde_core", - "typenum", ] [[package]] @@ -1627,17 +1419,6 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "hash32" version = "0.2.1" @@ -1795,24 +1576,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "hmac-sha256" -version = "1.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" -dependencies = [ - "digest 0.11.3", -] - -[[package]] -name = "hmac-sha512" -version = "1.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019ece39bbefc17f13f677a690328cb978dbf6790e141a3c24e66372cb38588b" -dependencies = [ - "digest 0.11.3", -] - [[package]] name = "hpke-rs" version = "0.7.0" @@ -2139,7 +1902,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -2427,7 +2190,7 @@ name = "libsignal-core" version = "0.1.0" source = "git+https://github.com/signalapp/libsignal?rev=44a6dd8fc9f9d6903b32842b80b3894a75678418#44a6dd8fc9f9d6903b32842b80b3894a75678418" dependencies = [ - "curve25519-dalek 5.0.0", + "curve25519-dalek", "derive_more", "displaydoc", "libsignal-debug", @@ -2654,15 +2417,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2682,12 +2436,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - [[package]] name = "num-integer" version = "0.1.46" @@ -2823,17 +2571,6 @@ dependencies = [ "log", ] -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "elliptic-curve", - "primeorder", - "sha2 0.10.9", -] - [[package]] name = "parking" version = "2.2.1" @@ -2885,15 +2622,6 @@ dependencies = [ "hmac 0.13.0", ] -[[package]] -name = "pem-rfc7468" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -2923,26 +2651,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkcs1" -version = "0.8.0-rc.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" -dependencies = [ - "der 0.8.1", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" -dependencies = [ - "der 0.8.1", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.33" @@ -3038,12 +2746,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3083,40 +2785,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "privacypass" -version = "0.2.0-pre.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5da5a5d50ec311674442d7388689094d044d47c783a2f3dd5cf4ef572be92a2" -dependencies = [ - "async-trait", - "base64", - "blind-rsa-signatures", - "generic-array 1.4.5", - "http", - "log", - "nom 8.0.0", - "p384", - "rand 0.10.2", - "serde", - "sha2 0.10.9", - "subtle", - "thiserror 2.0.19", - "tls_codec", - "trait-variant", - "typenum", - "voprf-ng", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -3521,26 +3189,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" -[[package]] -name = "rsa" -version = "0.10.0-rc.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" -dependencies = [ - "const-oid 0.10.2", - "crypto-bigint 0.7.5", - "crypto-primes", - "digest 0.11.3", - "pkcs1", - "pkcs8", - "rand_core 0.10.1", - "serde", - "serdect", - "signature", - "spki", - "zeroize", -] - [[package]] name = "rust_lib_twonly" version = "0.1.0" @@ -3567,11 +3215,9 @@ dependencies = [ "libc", "libsignal-protocol", "libsqlite3-sys", - "p384", "paste", "postcard", "pretty_env_logger", - "privacypass", "prost", "prost-build", "rand 0.8.7", @@ -3591,7 +3237,6 @@ dependencies = [ "tokio-tungstenite", "tokio-util", "tracing", - "tracing-appender", "tracing-subscriber", "uuid", "walkdir", @@ -3757,19 +3402,6 @@ dependencies = [ "sha2 0.11.0", ] -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct 0.2.0", - "der 0.7.10", - "generic-array 0.14.7", - "subtle", - "zeroize", -] - [[package]] name = "security-framework" version = "3.7.0" @@ -3854,16 +3486,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serdect" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" -dependencies = [ - "base16ct 1.0.0", - "serde", -] - [[package]] name = "sha1" version = "0.10.7" @@ -3965,16 +3587,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" -dependencies = [ - "digest 0.11.3", - "rand_core 0.10.1", -] - [[package]] name = "simd-adler32" version = "0.3.10" @@ -4045,23 +3657,13 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spki" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" -dependencies = [ - "base64ct", - "der 0.8.1", -] - [[package]] name = "spqr" version = "1.5.3" source = "git+https://github.com/signalapp/SparsePostQuantumRatchet.git?tag=v1.5.3#fd320484dcec89004021e6fdc7481825f5f261fa" dependencies = [ "cpufeatures 0.3.0", - "curve25519-dalek 5.0.0", + "curve25519-dalek", "displaydoc", "hax-lib", "hkdf 0.13.0", @@ -4181,7 +3783,7 @@ dependencies = [ "either", "futures-core", "futures-util", - "generic-array 0.14.7", + "generic-array", "log", "percent-encoding", "serde", @@ -4324,12 +3926,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "symlink" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" - [[package]] name = "syn" version = "2.0.119" @@ -4452,36 +4048,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "time" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tiny-skia" version = "0.11.4" @@ -4533,27 +4099,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tls_codec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18cc98286004cea38f717e2b03d990fc774fbfd38a82720de40e5c94365067c8" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "674f41dd95f76cdeb005c83f9444e20cf43daebef37cde9e49a9ca6f4e87b423" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "tokio" version = "1.53.1" @@ -4732,19 +4277,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-appender" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" -dependencies = [ - "crossbeam-channel", - "symlink", - "thiserror 2.0.19", - "time", - "tracing-subscriber", -] - [[package]] name = "tracing-attributes" version = "0.1.31" @@ -4795,17 +4327,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "trait-variant" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b19a4867a870f6edc4c283f2b455804b1879c0baf0e642f26b03ed8ee262d9d3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -4892,24 +4413,12 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - [[package]] name = "unicode-vo" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "universal-hash" version = "0.5.1" @@ -5016,25 +4525,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "voprf-ng" -version = "0.6.0-pre.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bed8944bdc5dfafa7b1c434c086b3f6281dfb26715bd8e42d58d5e4434cc0307" -dependencies = [ - "curve25519-dalek 4.1.3", - "derive-where", - "digest 0.10.7", - "displaydoc", - "elliptic-curve", - "generic-array 1.4.5", - "rand_core 0.10.1", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -5418,7 +4908,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" dependencies = [ - "curve25519-dalek 5.0.0", + "curve25519-dalek", "rand_core 0.10.1", "zeroize", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c52d52dd..4cc03848 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -36,7 +36,6 @@ sha2 = "0.10.8" aes-gcm = "0.10.3" chacha20poly1305 = "0.10.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -tracing-appender = "0.2.5" paste = "1.0.15" serde = { version = "1.0", features = ["derive"] } zeroize = { version = "1.8", features = ["derive"] } @@ -68,11 +67,6 @@ rustls = { version = "0.23.43", default-features = false, features = [ ] } tokio-util = "0.7.16" bon = "3.9.3" -privacypass = "=0.2.0-pre.3" -p384 = { version = "0.13.0", default-features = false, features = [ - "hash2curve", - "voprf", -] } stream-tungstenite = "0.6.1" image = { version = "0.25.10", default-features = false, features = ["png", "jpeg", "gif", "bmp", "webp"] } webp = "0.3.1" diff --git a/rust/build.rs b/rust/build.rs index a90c9925..97c47d40 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -28,7 +28,7 @@ fn main() -> Result<()> { prost_build::compile_protos(&["models/user_discovery.proto"], &["src/"])?; let client_proto_root = "src/api/proto/client"; let client_protos = [ - "src/api/proto/client/sealed_sender.proto", + "src/api/proto/client/transport.proto", "src/api/proto/client/groups.proto", "src/api/proto/client/messages.proto", "src/api/proto/client/data.proto", diff --git a/rust/src/api/messages/incoming.rs b/rust/src/api/messages/incoming.rs index 8c1e2bf5..23a57dc0 100644 --- a/rust/src/api/messages/incoming.rs +++ b/rust/src/api/messages/incoming.rs @@ -28,7 +28,6 @@ use crate::api::proto::{client_to_server, server_to_client}; use crate::context::Context; use crate::database::app::tables::{Contact, Group, Receipt}; use crate::error::{Result, TwonlyError}; -use crate::sealed_sender::SealedSender; use crate::services::contacts::ContactService; use client_to_server::response::{ok, Response}; use prost::Message as _; @@ -67,20 +66,6 @@ pub(crate) async fn handle_server_message( Kind::PendingMessagesV2(batch) => { return Ok(acknowledge_pending_messages(ctx, batch).await); } - Kind::SealedSenderMessage(message) => { - if let Err(error) = handle_sealed_message(ctx, message.body).await { - tracing::warn!("failed to process sealed-sender message: {error}"); - } - ok::Ok::None(true) - } - Kind::SealedSenderMessages(messages) => { - for message in messages.messages { - if let Err(error) = handle_sealed_message(ctx, message.body).await { - tracing::warn!("failed to process sealed-sender message in batch: {error}"); - } - } - ok::Ok::None(true) - } Kind::MailboxDrained(_) => { ctx.mark_mailbox_drained(); ok::Ok::None(true) @@ -168,16 +153,6 @@ pub(crate) async fn handle_new_server_message( handle_decoded_server_message(ctx, server_message.from_user_id, message).await } -pub(crate) async fn handle_sealed_message(ctx: &Arc, bytes: Vec) -> Result<()> { - let payload = SealedSender::decrypt(&bytes, ctx.as_ref()).await?; - - let message = payload - .message - .ok_or_else(|| TwonlyError::Generic("sealed message contains no client message".into()))?; - - handle_decoded_server_message(ctx, payload.from_user_id, message).await -} - pub(crate) async fn handle_request_new_pqc_prekeys( ctx: &Arc, ) -> Result { @@ -474,10 +449,6 @@ async fn handle_encrypted_inner( contact::check_for_profile_update(t, from_user_id, &content).await?; - if let Some(enabled) = content.sender_accepts_sealed_sender { - Contact::set_sealed_sender_enabled(t, from_user_id, enabled).await?; - } - if content.ask_for_friend_promotions == Some(true) { Contact::update_ask_for_friend_promotions(t, from_user_id).await?; } diff --git a/rust/src/api/messages/incoming/messages.rs b/rust/src/api/messages/incoming/messages.rs index e11cd459..578d8627 100644 --- a/rust/src/api/messages/incoming/messages.rs +++ b/rust/src/api/messages/incoming/messages.rs @@ -11,7 +11,6 @@ use crate::context::Context; use crate::database::app::tables::{Contact, MediaFile, NewReceipt, Receipt}; use crate::error::{Result, TwonlyError}; use crate::services::contacts::ContactService; -use crate::services::sealed_sender::SealedSenderService; use crate::utils::new_uuid_v4; use prost::Message as ProstMessage; use proto::encrypted_content::error_messages::Type; @@ -296,12 +295,9 @@ async fn encrypt_v2_with_session_recovery( } pub(crate) struct PreparedQueuedReceipt { - pub receipt_id: String, pub contact_id: i64, pub message_id: Option, pub contact_will_sends_receipt: i64, - /// The fully prepared client message. A sealed-sender envelope wraps this - /// value, while the named transport sends its encoding. pub message: proto::Message, pub wake_receiver: bool, } @@ -409,7 +405,6 @@ async fn prepare_queued_receipt_from_row( } Ok(PreparedQueuedReceipt { - receipt_id: receipt_id.to_owned(), contact_id: row.contact_id, message_id: row.message_id, contact_will_sends_receipt: row.contact_will_sends_receipt, @@ -474,24 +469,20 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc, receipt_id: &str) -> Err(error) => return Err(error), }; - let sent_sealed = SealedSenderService::try_send(ctx, &receipt).await?; - - if !sent_sealed { - match Server::send_text_message( - ctx, - receipt.contact_id, - receipt.message.encode_to_vec(), - receipt.wake_receiver, - ) - .await? - { - ServerResult::Ok(()) => {} - ServerResult::ErrorCode(code) => { - return Err(TwonlyError::Generic(format!( - "server rejected sender delivery receipt with code: {}", - code - ))); - } + match Server::send_text_message( + ctx, + receipt.contact_id, + receipt.message.encode_to_vec(), + receipt.wake_receiver, + ) + .await? + { + ServerResult::Ok(()) => {} + ServerResult::ErrorCode(code) => { + return Err(TwonlyError::Generic(format!( + "server rejected sender delivery receipt with code: {}", + code + ))); } } @@ -510,33 +501,6 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc, receipt_id: &str) -> .execute(&mut *t) .await?; - // The transport is per recipient: in a group one member may accept - // sealed envelopes while another still needs the named send. Recording - // it as an action keeps that distinction visible per member. - if sent_sealed { - sqlx::query!( - r#" - INSERT INTO message_actions(message_id, contact_id, type) - VALUES (?, ?, 'sealedSenderAt') - ON CONFLICT(message_id, contact_id, type) - DO UPDATE SET action_at = CAST(strftime('%s', 'now') AS INTEGER) - "#, - message_id, - receipt.contact_id, - ) - .execute(&mut *t) - .await?; - } else { - sqlx::query!( - "DELETE FROM message_actions - WHERE message_id = ? AND contact_id = ? AND type = 'sealedSenderAt'", - message_id, - receipt.contact_id, - ) - .execute(&mut *t) - .await?; - } - // `message_actions` keeps the per-recipient acknowledgement used by // group chats. The message row also carries the aggregate value used // by message bubbles and chat previews to leave the "sending" state. diff --git a/rust/src/api/messages/outgoing.rs b/rust/src/api/messages/outgoing.rs index b4f9b362..9067a3ff 100644 --- a/rust/src/api/messages/outgoing.rs +++ b/rust/src/api/messages/outgoing.rs @@ -23,10 +23,6 @@ pub(crate) async fn decorate_content( }; content.sender_profile_counter = Some(config.avatar_counter); - // Announced on every content, including when it is off: a contact that - // switches the feature off has to stop receiving sealed envelopes. - content.sender_accepts_sealed_sender = Some(config.sealed_sender_enabled); - if config.ask_for_friend_promotions { let database = ctx.app_db.read().await.clone(); let accepted = sqlx::query_scalar!("SELECT COUNT(*) FROM contacts WHERE accepted = 1") diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs index 1026e364..c77a95a7 100644 --- a/rust/src/api/mod.rs +++ b/rust/src/api/mod.rs @@ -7,7 +7,6 @@ pub(crate) mod groups; pub mod messages; pub mod proto; pub(super) mod runtime; -pub(crate) mod sealed_sender; pub(super) mod server; #[doc(hidden)] diff --git a/rust/src/api/proto/api/http/http_requests.proto b/rust/src/api/proto/api/http/http_requests.proto index b764c902..9e5e7011 100644 --- a/rust/src/api/proto/api/http/http_requests.proto +++ b/rust/src/api/proto/api/http/http_requests.proto @@ -1,18 +1,6 @@ syntax = "proto3"; package http_requests; -message SealedSenderMessageRequest { - reserved 4; - int64 recipient_user_id = 1; - bytes sealed_sender_message = 2; - bytes privacy_pass_token = 3; - bool wake_receiver = 5; -} - -message SealedSenderMessageResponse { - string message_id = 1; -} - message RequestUploadSlots { repeated string locally_known_slot_ids = 1; } diff --git a/rust/src/api/proto/api/websocket/client_to_server.proto b/rust/src/api/proto/api/websocket/client_to_server.proto index 0b3eb66e..4d18e056 100644 --- a/rust/src/api/proto/api/websocket/client_to_server.proto +++ b/rust/src/api/proto/api/websocket/client_to_server.proto @@ -259,16 +259,11 @@ message ApplicationData { message DisableMemoriesBackup {} - message GetPrivacyPassParameters {} - - message IssuePrivacyPassTokens { - repeated bytes token_requests = 1; - } - // Asks the server to (re)start a mailbox drain for the authenticated user. // The server responds immediately; messages arrive as PendingMessagesV2. message RequestPendingMessages {} + reserved 41, 42; oneof ApplicationData { TextMessage textMessage = 1; GetUserByUsername getUserByUsername = 2; @@ -298,8 +293,6 @@ message ApplicationData { DeleteMemory delete_memory = 38; DisableMemoriesBackup disable_memories_backup = 39; UploadPqcPreKeys upload_pqc_prekeys = 40; - GetPrivacyPassParameters get_privacy_pass_parameters = 41; - IssuePrivacyPassTokens issue_privacy_pass_tokens = 42; RequestPendingMessages request_pending_messages = 43; } } diff --git a/rust/src/api/proto/api/websocket/error.proto b/rust/src/api/proto/api/websocket/error.proto index 3fb85ae6..40024719 100644 --- a/rust/src/api/proto/api/websocket/error.proto +++ b/rust/src/api/proto/api/websocket/error.proto @@ -45,5 +45,5 @@ enum ErrorCode { ForegroundSessionConnected = 1036; NoRecoveryData = 1037; InvalidRecoveryData = 1038; - PrivacyPassQuotaExhausted = 1039; + reserved 1039; } diff --git a/rust/src/api/proto/api/websocket/server_to_client.proto b/rust/src/api/proto/api/websocket/server_to_client.proto index f9edc79d..4778e11b 100644 --- a/rust/src/api/proto/api/websocket/server_to_client.proto +++ b/rust/src/api/proto/api/websocket/server_to_client.proto @@ -11,6 +11,7 @@ message ServerToClient { message V0 { uint64 seq = 1; + reserved 9, 10; oneof Kind { Response response = 2; NewMessage newMessage = 3; @@ -18,8 +19,6 @@ message V0 { bool RequestNewPreKeys = 4; bool RequestNewPqcPreKeys = 8; error.ErrorCode error = 6; - SealedSenderMessage sealedSenderMessage = 9; - SealedSenderMessages sealedSenderMessages = 10; bool mailboxDrained = 11; PendingMessagesV2 pendingMessagesV2 = 12; } @@ -47,15 +46,6 @@ message PendingMessagesV2 { repeated PendingMessageV2 messages = 1; } -message SealedSenderMessage { - string message_id = 1; - bytes body = 2; -} - -message SealedSenderMessages { - repeated SealedSenderMessage messages = 1; -} - message Response { message Authenticated { @@ -200,21 +190,8 @@ message Response { int64 count = 3; } - message PrivacyPassParameters { - bytes token_challenge = 1; - bytes public_key = 2; - uint32 max_batch_size = 3; - uint32 max_age_seconds = 4; - uint32 daily_token_limit = 5; - uint32 issuance_cooldown_seconds = 6; - } - - message PrivacyPassTokenResponses { - repeated bytes token_responses = 1; - } - message Ok { - reserved 7, 11; + reserved 7, 11, 22, 23; oneof Ok { bool None = 1; int64 userid = 2; @@ -235,8 +212,6 @@ message Response { MemoriesList memories_list = 19; MemoriesUrl memories_url = 20; MemoriesUsage memories_usage = 21; - PrivacyPassParameters privacy_pass_parameters = 22; - PrivacyPassTokenResponses privacy_pass_token_responses = 23; } } diff --git a/rust/src/api/proto/client/messages.proto b/rust/src/api/proto/client/messages.proto index b3be5d49..2eaa0d94 100644 --- a/rust/src/api/proto/client/messages.proto +++ b/rust/src/api/proto/client/messages.proto @@ -9,9 +9,7 @@ message EncryptedContent { optional int64 sender_profile_counter = 4; optional bytes sender_user_discovery_version = 21; optional bool ask_for_friend_promotions = 25; - /// Announces that this sender accepts sealed-sender envelopes. Peers must - /// keep using the named transport until they have seen this flag. - optional bool sender_accepts_sealed_sender = 28; + reserved 28; optional MessageUpdate message_update = 5; optional Media media = 6; diff --git a/rust/src/api/proto/client/sealed_sender.proto b/rust/src/api/proto/client/sealed_sender.proto deleted file mode 100644 index 9a0f8f03..00000000 --- a/rust/src/api/proto/client/sealed_sender.proto +++ /dev/null @@ -1,90 +0,0 @@ -syntax = "proto3"; - -message EncryptedMessageEnvelope { - // Fresh 32-byte X25519 public key generated for this envelope. The sender - // derives the encryption key from this ephemeral key pair and the recipient's - // public identity key using HKDF-SHA-256 with an empty salt and a 32-byte - // output. HKDF info must be the UTF-8 bytes of - // "twonly-message-envelope-encryption-v1" followed by ephemeral_public_key - // and the recipient's serialized public identity key. - bytes ephemeral_public_key = 1; - - // Fresh 24-byte XChaCha20-Poly1305 nonce. - bytes nonce = 2; - - // Serialized MessageEnvelope encrypted with XChaCha20-Poly1305. This includes - // the 16-byte Poly1305 authentication tag. The AEAD associated data is exactly - // the UTF-8 bytes of "twonly-message-envelope-encryption-v1". - bytes ciphertext = 3; -} - -message MessageEnvelope { - // The serialized MessageEnvelope is encrypted into EncryptedMessageEnvelope - // before it is sent to the server. - - // Exact serialized MessageEnvelopePayload bytes. The signature is calculated - // directly over these bytes. Keeping the serialized payload allows verification - // without relying on canonical protobuf serialization. - bytes signed_payload = 1; - - // Signature created with the sender's identity key. For a known contact, use - // the pinned public identity key. For an unknown sender, fetch the public key - // from the trusted server, verify this signature, and only then pin the key and - // process the raw message. - bytes signature = 2; -} - -message MessageEnvelopePayload { - // Domain-separation and protocol-version marker. The receiver must reject the - // payload unless this is exactly "twonly-message-envelope-v1". - string magic = 1; - - // The sender is protected by the encryption of the outer MessageEnvelope. - int64 from_user_id = 2; - - // Bind the sender's signature to the intended recipient. This prevents a - // recipient from re-encrypting a valid signed envelope for another user. The - // binding is required because raw_message is not always a Signal ciphertext - // and therefore is not always independently bound to a Signal session. The - // receiver must reject an envelope when this ID does not match its own user ID. - int64 recipient_user_id = 3; - - // The mandatory UUIDv4 receipt_id is covered by the envelope signature and is - // used by the receiver to reject replayed messages before processing them. - Message message = 4; - - // UTC Unix timestamp in seconds when the envelope was created. The receiver - // must reject envelopes older than 45 days or more than five minutes in the - // future. This field is covered by the envelope signature. - int64 created_at_unix_seconds = 5; -} - -message Message { - enum Type { - SENDER_DELIVERY_RECEIPT = 0; - PLAINTEXT_CONTENT = 1; - CIPHERTEXT = 2; - PREKEY_BUNDLE = 3; - TEST_NOTIFICATION = 4; - CIPHERTEXT_V2 = 5; - } - Type type = 1; - string receipt_id = 2; - optional bytes encrypted_content = 3; - optional PlaintextContent plaintext_content = 4; -} - -message PlaintextContent { - optional DecryptionErrorMessage decryption_error_message = 1; - optional RetryErrorMessage retry_control_error = 2; - - message RetryErrorMessage { } - - message DecryptionErrorMessage { - enum Type { - UNKNOWN = 0; - PREKEY_UNKNOWN = 1; - } - Type type = 1; - } -} diff --git a/rust/src/api/proto/client/transport.proto b/rust/src/api/proto/client/transport.proto new file mode 100644 index 00000000..d77400f0 --- /dev/null +++ b/rust/src/api/proto/client/transport.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +message Message { + enum Type { + SENDER_DELIVERY_RECEIPT = 0; + PLAINTEXT_CONTENT = 1; + CIPHERTEXT = 2; + PREKEY_BUNDLE = 3; + TEST_NOTIFICATION = 4; + CIPHERTEXT_V2 = 5; + } + Type type = 1; + string receipt_id = 2; + optional bytes encrypted_content = 3; + optional PlaintextContent plaintext_content = 4; +} + +message PlaintextContent { + optional DecryptionErrorMessage decryption_error_message = 1; + optional RetryErrorMessage retry_control_error = 2; + + message RetryErrorMessage { } + + message DecryptionErrorMessage { + enum Type { + UNKNOWN = 0; + PREKEY_UNKNOWN = 1; + } + Type type = 1; + } +} diff --git a/rust/src/api/runtime/helpers.rs b/rust/src/api/runtime/helpers.rs index ea15e9a8..594a775b 100644 --- a/rust/src/api/runtime/helpers.rs +++ b/rust/src/api/runtime/helpers.rs @@ -14,7 +14,6 @@ use crate::error::{Result, TwonlyError}; use crate::services::direct_media_upload::DirectMediaUploadService; use crate::services::groups::GroupService; use crate::services::mediafiles::MediaFileService; -use crate::services::privacy_pass::PrivacyPassTokens; use prost::Message as ProstMessage; use std::future::Future; use std::pin::Pin; @@ -62,12 +61,6 @@ pub(crate) fn schedule_post_authentication(ctx: &Arc, in_background: bo tracing::warn!("failed to replay legacy raw-byte outbox: {error}"); } - // Minting has to happen on an authenticated socket, so the pool is - // topped up while one is available rather than when a message needs it. - if let Err(error) = PrivacyPassTokens::refill_if_needed(&ctx).await { - tracing::warn!("failed to refill the Privacy Pass token pool: {error}"); - } - // Runs before the receipt sweep below: without a bundle on the server // no peer can open a session with this account, so anything they have // queued for us stays stuck. diff --git a/rust/src/api/sealed_sender.rs b/rust/src/api/sealed_sender.rs deleted file mode 100644 index 311d4ea3..00000000 --- a/rust/src/api/sealed_sender.rs +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -//! Anonymous upload endpoint for sealed-sender envelopes. -//! -//! Unlike every other API call this one carries no session and no -//! authentication header: the whole point is that the server must not learn who -//! sent the envelope. The Privacy Pass token in the request is what keeps the -//! endpoint from becoming an open relay. - -use crate::api::proto::http_requests::{SealedSenderMessageRequest, SealedSenderMessageResponse}; -use crate::bridge::api::RustApi; -use crate::error::{Result, TwonlyError}; -use prost::Message as _; -use std::sync::LazyLock; - -/// One shared client, so a sealed upload reuses a pooled connection instead of -/// paying for a TLS handshake per message. -static CLIENT: LazyLock = LazyLock::new(|| { - reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .expect("the sealed-sender HTTP client must be constructible") -}); - -pub(crate) struct SealedSenderApi; - -impl SealedSenderApi { - pub(crate) async fn upload( - recipient_user_id: i64, - sealed_sender_message: Vec, - privacy_pass_token: Vec, - wake_receiver: bool, - ) -> Result { - let request = SealedSenderMessageRequest { - recipient_user_id, - sealed_sender_message, - privacy_pass_token, - wake_receiver, - }; - - let response = CLIENT - .post(format!( - "{}sealed-sender/messages", - RustApi::api_base_url("https".into()) - )) - .header("Content-Type", "application/x-protobuf") - .body(request.encode_to_vec()) - .send() - .await - .map_err(|error| TwonlyError::Generic(error.to_string()))?; - - let status = response.status(); - if !status.is_success() { - return Err(TwonlyError::Generic(format!( - "sealed-sender upload returned {status}" - ))); - } - - let body = response - .bytes() - .await - .map_err(|error| TwonlyError::Generic(error.to_string()))?; - Ok(SealedSenderMessageResponse::decode(body) - .map_err(|error| TwonlyError::Generic(error.to_string()))? - .message_id) - } -} diff --git a/rust/src/api/server.rs b/rust/src/api/server.rs index 02c16ed3..2d18b1b0 100644 --- a/rust/src/api/server.rs +++ b/rust/src/api/server.rs @@ -8,7 +8,6 @@ mod contacts; mod memories; mod passwordless; pub mod prekeys; -mod privacy_pass; mod purchases; mod transport; diff --git a/rust/src/api/server/privacy_pass.rs b/rust/src/api/server/privacy_pass.rs deleted file mode 100644 index 246e5570..00000000 --- a/rust/src/api/server/privacy_pass.rs +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -use super::Server; -use crate::api::proto::client_to_server; -use crate::api::proto::server_to_client::response::ok::Ok as ResponseOk; -use crate::api::proto::server_to_client::response::PrivacyPassParameters; -use crate::api::runtime::helpers::decode_ok_value; -use crate::bridge::api::ServerResult; -use crate::context::Context; -use crate::error::Result; -use std::sync::Arc; - -impl Server { - /// Loads the current issuance parameters: the daily token challenge, the - /// issuer public key and the limits the client has to stay inside. - pub(crate) async fn get_privacy_pass_parameters( - ctx: &Arc, - ) -> Result> { - let bytes = Self::application( - ctx, - client_to_server::application_data::ApplicationData::GetPrivacyPassParameters( - client_to_server::application_data::GetPrivacyPassParameters {}, - ), - ) - .await?; - decode_ok_value(bytes, |value| match value { - ResponseOk::PrivacyPassParameters(parameters) => Some(parameters), - _ => None, - }) - } - - /// Exchanges blinded token requests for the server's blinded evaluations. - /// - /// This runs over the authenticated socket, so the server counts the tokens - /// against this account's daily quota. The tokens themselves are unlinkable - /// to it once they are finalized. - pub(crate) async fn issue_privacy_pass_tokens( - ctx: &Arc, - token_requests: Vec>, - ) -> Result>>> { - let bytes = Self::application( - ctx, - client_to_server::application_data::ApplicationData::IssuePrivacyPassTokens( - client_to_server::application_data::IssuePrivacyPassTokens { token_requests }, - ), - ) - .await?; - decode_ok_value(bytes, |value| match value { - ResponseOk::PrivacyPassTokenResponses(responses) => Some(responses.token_responses), - _ => None, - }) - } -} diff --git a/rust/src/bridge/callbacks.rs b/rust/src/bridge/callbacks.rs index fb14734d..1657b98e 100644 --- a/rust/src/bridge/callbacks.rs +++ b/rust/src/bridge/callbacks.rs @@ -3,12 +3,11 @@ * */ -pub(crate) mod log; mod macros; use flutter_rust_bridge::DartFnFuture; +use crate::callback_generator; use crate::error::{Result, TwonlyError}; -use crate::{callback_generator, frb_generated::StreamSink}; use std::sync::Arc; use std::collections::HashMap; @@ -23,9 +22,6 @@ pub(crate) static FLUTTER_CALLBACKS: std::sync::RwLock StreamSink - }, Api api { verification_succeeded: (i64) => (), user_config_changed: (crate::user_config::UserConfig) => () @@ -47,12 +43,12 @@ pub(crate) fn get_callbacks() -> Result { } } - // Fallback: if not in a scoped tokio task or if the specific callback_id isn't found, - // we pick the first available callbacks from the map. This gracefully handles - // tracing initialization which happens outside of any scoped task. - if let Some((_, cb)) = map.iter().next() { + // Incoming API events are not always associated with the Flutter call + // that started their work. Preserve the existing fallback for those API + // callbacks; logging no longer depends on this path. + if let Some((_, callbacks)) = map.iter().next() { tracing::warn!("FlutterCallbacks fallback used: No CURRENT_CALLBACK_ID scope was found, or the ID was missing from the map. Using an arbitrary callback. This may lead to race conditions if multiple isolates are active."); - return Ok(cb.clone()); + return Ok(callbacks.clone()); } Err(TwonlyError::MissingCallbackInitialization) diff --git a/rust/src/bridge/callbacks/log.rs b/rust/src/bridge/callbacks/log.rs deleted file mode 100644 index 9ddebde6..00000000 --- a/rust/src/bridge/callbacks/log.rs +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -use crate::frb_generated::StreamSink; -use std::sync::RwLock; -use tracing_subscriber::fmt::MakeWriter; - -/// The sink is bound to the native port of the Dart isolate that handed it to -/// us. That isolate can go away while the process (and therefore the tracing -/// subscriber) lives on -- a hot restart, an engine restart, or a background -/// isolate that finished its task. Keeping the sink behind a lock lets a newly -/// initialized isolate take over the log stream instead of writing into a dead -/// port forever. -static DART_SINK: RwLock>> = RwLock::new(None); - -pub(crate) fn set_dart_sink(sink: StreamSink) { - if let Ok(mut guard) = DART_SINK.write() { - *guard = Some(sink); - } -} - -pub fn strip_ansi(input: &str) -> String { - let mut out = String::with_capacity(input.len()); - let mut chars = input.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '\x1b' { - if let Some(&next) = chars.peek() { - if next == '[' { - // CSI sequence: ESC [ ... [final byte 0x40..=0x7e] - chars.next(); // consume '[' - while let Some(&ch) = chars.peek() { - chars.next(); - if ('@'..='~').contains(&ch) { - break; - } - } - continue; - } else if next == ']' { - // OSC sequence: ESC ] ... (BEL \x07 or ESC \) - chars.next(); // consume ']' - while let Some(ch) = chars.next() { - if ch == '\x07' { - break; - } - if ch == '\x1b' && chars.peek() == Some(&'\\') { - chars.next(); - break; - } - } - continue; - } else if ('@'..='_').contains(&next) { - // 2-character escape sequence - chars.next(); - continue; - } - } - } else if c == '\\' && chars.peek() == Some(&'^') { - let mut clone = chars.clone(); - clone.next(); // '^' - if clone.next() == Some('[') { - if clone.peek() == Some(&'[') { - clone.next(); - } - let mut valid = false; - while let Some(ch) = clone.next() { - if ('@'..='~').contains(&ch) { - valid = true; - break; - } else if !('0'..='?').contains(&ch) && !(' '..='/').contains(&ch) { - break; - } - } - if valid { - chars.next(); // consume '^' - chars.next(); // consume '[' - if chars.peek() == Some(&'[') { - chars.next(); - } - while let Some(&ch) = chars.peek() { - chars.next(); - if ('@'..='~').contains(&ch) { - break; - } - } - continue; - } - } - } else if c == '^' && chars.peek() == Some(&'[') { - let mut clone = chars.clone(); - clone.next(); // '[' - if clone.peek() == Some(&'[') { - clone.next(); - } - let mut valid = false; - while let Some(ch) = clone.next() { - if ('@'..='~').contains(&ch) { - valid = true; - break; - } else if !('0'..='?').contains(&ch) && !(' '..='/').contains(&ch) { - break; - } - } - if valid { - chars.next(); // consume '[' - if chars.peek() == Some(&'[') { - chars.next(); - } - while let Some(&ch) = chars.peek() { - chars.next(); - if ('@'..='~').contains(&ch) { - break; - } - } - continue; - } - } - out.push(c); - } - out -} - -#[derive(Clone, Copy)] -pub(crate) struct DartWriter; - -impl std::io::Write for DartWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - if let Ok(msg) = std::str::from_utf8(buf) { - let clean = strip_ansi(msg.trim_end()); - let failed = match DART_SINK.read() { - Ok(guard) => match guard.as_ref() { - Some(sink) => sink.add(clean).is_err(), - None => false, - }, - Err(_) => false, - }; - // The isolate behind the sink is gone. Drop it so we stop paying - // for a send on every log line until a new isolate registers. - if failed { - if let Ok(mut guard) = DART_SINK.write() { - *guard = None; - } - } - } - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -impl<'a> MakeWriter<'a> for DartWriter { - type Writer = DartWriter; - - fn make_writer(&'a self) -> Self::Writer { - *self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_strip_ansi_raw() { - let input = - "\x1b[3mreceipt_id\x1b[0m\x1b[2m=\x1b[0m\"d9891084-0f7c-4f30-958d-d8619df5a91c\""; - assert_eq!( - strip_ansi(input), - "receipt_id=\"d9891084-0f7c-4f30-958d-d8619df5a91c\"" - ); - } - - #[test] - fn test_strip_ansi_escaped_caret() { - let input = r#"\^[[3mreceipt_id\^[[0m\^[[2m=\^[[0m"d9891084-0f7c-4f30-958d-d8619df5a91c" \^[[3muser\^[[0m\^[[2m=\^[[0m2214489137315557376 \^[[3mkind\^[[0m\^[[2m=\^[[0m"FlameSync" Handling incoming message: FlameSync"#; - assert_eq!( - strip_ansi(input), - r#"receipt_id="d9891084-0f7c-4f30-958d-d8619df5a91c" user=2214489137315557376 kind="FlameSync" Handling incoming message: FlameSync"# - ); - } - - #[test] - fn test_strip_ansi_caret() { - let input = "^[[3mreceipt_id^[[0m^[[2m=^[[0m\"test\""; - assert_eq!(strip_ansi(input), "receipt_id=\"test\""); - } - - #[test] - fn test_strip_ansi_plain_text() { - assert_eq!(strip_ansi("hello world 123"), "hello world 123"); - } -} diff --git a/rust/src/bridge/logging.rs b/rust/src/bridge/logging.rs new file mode 100644 index 00000000..9ef3a9cc --- /dev/null +++ b/rust/src/bridge/logging.rs @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, Tobias Müller git@tsmr.eu + * + */ + +use crate::error::{Result, TwonlyError}; +use flutter_rust_bridge::frb; + +#[derive(Clone, Copy, Debug)] +pub enum LogLevel { + Finest, + Fine, + Info, + Warning, + Shout, +} + +/// Adds a Dart record to the Rust-owned application log. +/// +/// This only performs a short, serialized append and is synchronous so Dart +/// records cannot be reordered by a collection of unawaited futures. +#[frb(sync)] +pub fn write_log( + level: LogLevel, + source: String, + message: String, + in_background: bool, +) -> Result<()> { + crate::log::write_dart_log(level, &source, &message, in_background) +} + +pub async fn load_log_file() -> Result { + tokio::task::spawn_blocking(crate::log::load_log_file) + .await + .map_err(|error| TwonlyError::Generic(error.to_string()))? +} + +pub async fn read_last_log_lines(line_count: u32) -> Result { + tokio::task::spawn_blocking(move || crate::log::read_last_log_lines(line_count as usize)) + .await + .map_err(|error| TwonlyError::Generic(error.to_string()))? +} + +pub async fn clean_log_file() -> Result<()> { + tokio::task::spawn_blocking(crate::log::clean_log_file) + .await + .map_err(|error| TwonlyError::Generic(error.to_string()))? +} + +/// Truncates `app.log` through its owner instead of unlinking an open file. +pub async fn clear_log_file() -> Result { + tokio::task::spawn_blocking(crate::log::clear_log_file) + .await + .map_err(|error| TwonlyError::Generic(error.to_string()))? +} diff --git a/rust/src/bridge/mod.rs b/rust/src/bridge/mod.rs index f949aa02..252d42b4 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 logging; pub mod user_config; pub mod wrapper; diff --git a/rust/src/context.rs b/rust/src/context.rs index a982d358..ae801893 100644 --- a/rust/src/context.rs +++ b/rust/src/context.rs @@ -16,9 +16,11 @@ use crate::secure_storage::SecureStorage; use crate::signal::engine::RustSignalEngine; use crate::user_discovery::UserDiscovery; use crate::utils::Shared; -use libsignal_protocol::IdentityKey; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::{path::PathBuf, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use tokio::sync::{Mutex, Notify, OnceCell, RwLock}; use zeroize::Zeroize; @@ -45,11 +47,6 @@ pub struct Context { mailbox_drained: Notify, incoming_generation: AtomicU64, incoming_committed: Notify, - /// Serializes Privacy Pass minting and holds the earliest time the issuer - /// may be asked again. It belongs to the account, not to the process: two - /// contexts in one process have separate quotas and must not block or back - /// each other off. - pub(crate) privacy_pass_issuance: Mutex>, /// Set once this connection has confirmed the account has a PQC prekey /// bundle on the server (or has just published one). Per context rather /// than process-wide so two accounts in one process check independently. @@ -130,7 +127,6 @@ impl Context { mailbox_drained: Notify::new(), incoming_generation: AtomicU64::new(0), incoming_committed: Notify::new(), - privacy_pass_issuance: Mutex::new(None), pqc_bundle_verified: AtomicBool::new(false), }); ApiRuntime::initialize(&ctx).await?; @@ -192,11 +188,13 @@ impl Context { std::fs::create_dir_all(&config.database_dir)?; std::fs::create_dir_all(&config.data_dir)?; - // Ahead of the already-initialized check: the context is a process-wide - // OnceCell, but the calling isolate may be a new one that has to hand - // tracing a live log sink. - let log_dir = PathBuf::from(&config.data_dir).join("log"); - init_tracing(&log_dir, runtime_mode == RuntimeMode::Flutter).await; + // Logging is process-wide and owns app.log directly. Initialize it + // before the context check so notification and Flutter runtimes both + // have a sink even when the main context already exists. + init_tracing( + Path::new(&config.data_dir), + runtime_mode != RuntimeMode::Flutter, + ); if GLOBAL_CONTEXT.initialized() { tracing::info!("twonly already initialized. Ensuring storage directories exist."); @@ -291,7 +289,6 @@ impl Context { mailbox_drained: Notify::new(), incoming_generation: AtomicU64::new(0), incoming_committed: Notify::new(), - privacy_pass_issuance: Mutex::new(None), pqc_bundle_verified: AtomicBool::new(false), }); if let Err(error) = ctx.initialize_user_discovery_from_config().await { @@ -333,7 +330,6 @@ impl Context { mailbox_drained: Notify::new(), incoming_generation: AtomicU64::new(0), incoming_committed: Notify::new(), - privacy_pass_issuance: Mutex::new(None), pqc_bundle_verified: AtomicBool::new(false), }); if let Err(error) = ctx.initialize_user_discovery_from_config().await { @@ -401,23 +397,6 @@ impl Context { .ok_or_else(|| TwonlyError::Generic("local user ID is missing".into())) } - pub(crate) async fn get_identity(&self, user_id: i64) -> Result> { - let database = self.rust_db.read().await.clone(); - let user_id = user_id.to_string(); - let identity_key = sqlx::query_scalar!( - r#"SELECT identity_key FROM signal_identities WHERE name = ?"#, - user_id, - ) - .fetch_optional(&database.pool) - .await?; - - identity_key - .map(|bytes| { - IdentityKey::decode(&bytes).map_err(|error| TwonlyError::Signal(error.to_string())) - }) - .transpose() - } - pub(crate) async fn replace_rust_database( &self, database: Database, diff --git a/rust/src/database/app/migrations/0007_remove_experimental_transport.sql b/rust/src/database/app/migrations/0007_remove_experimental_transport.sql new file mode 100644 index 00000000..11f5702a --- /dev/null +++ b/rust/src/database/app/migrations/0007_remove_experimental_transport.sql @@ -0,0 +1,3 @@ +DROP TABLE privacy_pass_tokens; + +ALTER TABLE contacts DROP COLUMN sealed_sender_enabled; diff --git a/rust/src/database/app/tables/contact.rs b/rust/src/database/app/tables/contact.rs index 971b53f2..527dca74 100644 --- a/rust/src/database/app/tables/contact.rs +++ b/rust/src/database/app/tables/contact.rs @@ -38,7 +38,6 @@ pub struct Contact { pub ask_for_friend_promotions: Option, pub media_send_counter: i64, pub media_received_counter: i64, - pub sealed_sender_enabled: i64, } #[derive(bon::Builder)] @@ -183,27 +182,6 @@ impl Contact { Ok(()) } - /// Records whether a contact announced that it accepts sealed-sender - /// envelopes. A peer that stops announcing it goes back to named sends. - pub async fn set_sealed_sender_enabled( - t: &mut Transaction<'_, Sqlite>, - user_id: i64, - enabled: bool, - ) -> Result<()> { - // Announced on every encrypted content, so the write is skipped unless - // the value actually changes. - sqlx::query!( - r#"UPDATE contacts SET sealed_sender_enabled = ? - WHERE user_id = ? AND sealed_sender_enabled != ?"#, - enabled, - user_id, - enabled, - ) - .execute(&mut **t) - .await?; - Ok(()) - } - pub async fn is_user_discovery_allowed( context: &Context, t: &mut Transaction<'_, Sqlite>, diff --git a/rust/src/error.rs b/rust/src/error.rs index c687296e..f84dc22c 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -123,9 +123,6 @@ pub enum TwonlyError { #[error("Unknown protobuf enum value: {0}")] UnknownProtobufEnumValue(#[from] prost::UnknownEnumValue), - #[error(transparent)] - SealedSender(#[from] crate::sealed_sender::SealedSenderError), - #[error("{0}")] HexError(#[from] FromHexError), diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 7f7d6f2f..446646c1 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 = 640399573; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 174490644; // Section: executor @@ -124,6 +124,76 @@ fn wire__crate__bridge__groups__add_new_group_members_impl( }, ) } +fn wire__crate__bridge__logging__clean_log_file_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: "clean_log_file", + 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::logging::clean_log_file().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__bridge__logging__clear_log_file_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: "clear_log_file", + 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::logging::clear_log_file().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__groups__create_new_group_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -349,10 +419,6 @@ fn wire__crate__bridge__callbacks__init_flutter_callbacks_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_callback_id = ::sse_decode(&mut deserializer); - let api_logging_get_stream_sink = - decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException( - ::sse_decode(&mut deserializer), - ); let api_api_verification_succeeded = decode_DartFn_Inputs_i_64_Output_unit_AnyhowException( ::sse_decode(&mut deserializer), @@ -367,7 +433,6 @@ fn wire__crate__bridge__callbacks__init_flutter_callbacks_impl( let output_ok = Result::<_, ()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks( api_callback_id, - api_logging_get_stream_sink, api_api_verification_succeeded, api_api_user_config_changed, ); @@ -488,6 +553,41 @@ fn wire__crate__bridge__groups__leave_group_impl( }, ) } +fn wire__crate__bridge__logging__load_log_file_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: "load_log_file", + 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::logging::load_log_file().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__groups__manage_admin_state_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -531,6 +631,43 @@ fn wire__crate__bridge__groups__manage_admin_state_impl( }, ) } +fn wire__crate__bridge__logging__read_last_log_lines_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: "read_last_log_lines", + 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_line_count = ::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::logging::read_last_log_lines(api_line_count).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__groups__remove_member_from_group_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -4779,40 +4916,6 @@ fn wire__crate__bridge__wrapper__rust_utils_recover_secret_impl( }, ) } -fn wire__crate__bridge__callbacks__log__strip_ansi_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_normal::( - flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "strip_ansi", - 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_input = ::sse_decode(&mut deserializer); - deserializer.end(); - move |context| { - transform_result_sse::<_, ()>((move || { - let output_ok = - Result::<_, ()>::Ok(crate::bridge::callbacks::log::strip_ansi(&api_input))?; - Ok(output_ok) - })()) - } - }, - ) -} fn wire__crate__bridge__groups__update_chat_deletion_time_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -5121,45 +5224,49 @@ fn wire__crate__bridge__user_config__user_config_api_update_impl( }, ) } +fn wire__crate__bridge__logging__write_log_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: "write_log", + 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_level = ::sse_decode(&mut deserializer); + let api_source = ::sse_decode(&mut deserializer); + let api_message = ::sse_decode(&mut deserializer); + let api_in_background = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = crate::bridge::logging::write_log( + api_level, + api_source, + api_message, + api_in_background, + )?; + Ok(output_ok) + })(), + ) + }, + ) +} // Section: related_funcs -fn decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException( - dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn() -> flutter_rust_bridge::DartFnFuture< - StreamSink, -> { - use flutter_rust_bridge::IntoDart; - - async fn body( - dart_opaque: flutter_rust_bridge::DartOpaque, - ) -> StreamSink { - 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<()> { @@ -5275,14 +5382,6 @@ impl SseDecode for std::collections::HashMap> { } } -impl SseDecode for StreamSink { - // 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 StreamSink::deserialize(inner); - } -} - impl SseDecode for StreamSink { @@ -5797,6 +5896,21 @@ impl SseDecode for Vec { } } +impl SseDecode for crate::bridge::logging::LogLevel { + // 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::bridge::logging::LogLevel::Finest, + 1 => crate::bridge::logging::LogLevel::Fine, + 2 => crate::bridge::logging::LogLevel::Info, + 3 => crate::bridge::logging::LogLevel::Warning, + 4 => crate::bridge::logging::LogLevel::Shout, + _ => unreachable!("Invalid variant for LogLevel: {}", inner), + }; + } +} + impl SseDecode for crate::services::media_upload::MediaSizeReport { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -6255,7 +6369,6 @@ impl SseDecode for crate::user_config::UserConfig { let mut var_storeMediaFilesInGallery = ::sse_decode(deserializer); let mut var_autoStoreAllSendUnlimitedMediaFiles = ::sse_decode(deserializer); let mut var_typingIndicators = ::sse_decode(deserializer); - let mut var_sealedSenderEnabled = ::sse_decode(deserializer); let mut var_showRestoreFlame = ::sse_decode(deserializer); let mut var_myBestFriendGroupId = >::sse_decode(deserializer); let mut var_signalLastSignedPreKeyUpdated = @@ -6321,7 +6434,6 @@ impl SseDecode for crate::user_config::UserConfig { store_media_files_in_gallery: var_storeMediaFilesInGallery, auto_store_all_send_unlimited_media_files: var_autoStoreAllSendUnlimitedMediaFiles, typing_indicators: var_typingIndicators, - sealed_sender_enabled: var_sealedSenderEnabled, show_restore_flame: var_showRestoreFlame, my_best_friend_group_id: var_myBestFriendGroupId, signal_last_signed_pre_key_updated: var_signalLastSignedPreKeyUpdated, @@ -6380,141 +6492,144 @@ fn pde_ffi_dispatcher_primary_impl( match func_id { 1 => wire__crate__bridge__groups__add_hidden_contact_impl(port, ptr, rust_vec_len, data_len), 2 => wire__crate__bridge__groups__add_new_group_members_impl(port, ptr, rust_vec_len, data_len), -3 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len), -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_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_abandon_media_impl(port, ptr, rust_vec_len, data_len), -17 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len), -18 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len), -20 => wire__crate__bridge__api__rust_api_authentication_headers_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_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len), -26 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len), -27 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len), -28 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), -29 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), -30 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), -31 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len), -32 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len), -34 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), -35 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), -36 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), -37 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), -38 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), -39 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), -40 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len), -41 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len), -42 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), -43 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len), -44 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), -45 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), -46 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), -47 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), -48 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), -49 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -50 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), -51 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), -52 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), -53 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), -54 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), -55 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), -56 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), -57 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), -58 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), -59 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), -60 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), -61 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), -62 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), -63 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), -64 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), -65 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), -66 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), -67 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), -68 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), -69 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), -70 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), -71 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), -72 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), -73 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), -74 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), -75 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), -76 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), -77 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), -78 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), -79 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), -80 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), -81 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), -82 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), -83 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), -84 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), -85 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), -86 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), -87 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), -88 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), -89 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), -90 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), -91 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), -92 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), -93 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), -94 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), -95 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), -96 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), -97 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), -98 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), -99 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), -100 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), -101 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), -102 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), -103 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), -104 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), -105 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), -106 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), -107 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), -108 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), -109 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), -110 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -111 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), -112 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -113 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), -114 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), -115 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -116 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), -117 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), -118 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), -119 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), -120 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), -121 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), -122 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), -123 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), -124 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), -125 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), -126 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), -127 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), -128 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), -129 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), -130 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), -131 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), -132 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), -133 => wire__crate__bridge__callbacks__log__strip_ansi_impl(port, ptr, rust_vec_len, data_len), -134 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), -135 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), -137 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), -138 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), -139 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), -140 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), -141 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), +3 => wire__crate__bridge__logging__clean_log_file_impl(port, ptr, rust_vec_len, data_len), +4 => wire__crate__bridge__logging__clear_log_file_impl(port, ptr, rust_vec_len, data_len), +5 => wire__crate__bridge__groups__create_new_group_impl(port, ptr, rust_vec_len, data_len), +6 => wire__crate__bridge__groups__fetch_group_state_impl(port, ptr, rust_vec_len, data_len), +7 => wire__crate__bridge__groups__fetch_group_states_for_unjoined_groups_impl(port, ptr, rust_vec_len, data_len), +8 => wire__crate__bridge__groups__fetch_missing_group_public_keys_impl(port, ptr, rust_vec_len, data_len), +9 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_change_exclusion_for_contact_impl(port, ptr, rust_vec_len, data_len), +10 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_get_current_version_impl(port, ptr, rust_vec_len, data_len), +11 => wire__crate__bridge__wrapper__user_discovery__flutter_user_discovery_update_verification_state_for_user_impl(port, ptr, rust_vec_len, data_len), +12 => wire__crate__bridge__callbacks__init_flutter_callbacks_impl(port, ptr, rust_vec_len, data_len), +13 => wire__crate__bridge__initialize_twonly_flutter_impl(port, ptr, rust_vec_len, data_len), +14 => wire__crate__bridge__initialize_twonly_standalone_impl(port, ptr, rust_vec_len, data_len), +15 => wire__crate__bridge__groups__leave_group_impl(port, ptr, rust_vec_len, data_len), +16 => wire__crate__bridge__logging__load_log_file_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__logging__read_last_log_lines_impl(port, ptr, rust_vec_len, data_len), +19 => wire__crate__bridge__groups__remove_member_from_group_impl(port, ptr, rust_vec_len, data_len), +20 => wire__crate__bridge__api__rust_api_abandon_media_impl(port, ptr, rust_vec_len, data_len), +21 => wire__crate__bridge__api__rust_api_add_additional_user_impl(port, ptr, rust_vec_len, data_len), +22 => wire__crate__bridge__api__rust_api_allocate_sequence_impl(port, ptr, rust_vec_len, data_len), +24 => wire__crate__bridge__api__rust_api_authentication_headers_impl(port, ptr, rust_vec_len, data_len), +26 => wire__crate__bridge__api__rust_api_change_username_impl(port, ptr, rust_vec_len, data_len), +27 => wire__crate__bridge__api__rust_api_check_for_deleted_usernames_impl(port, ptr, rust_vec_len, data_len), +28 => wire__crate__bridge__api__rust_api_check_for_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), +29 => wire__crate__bridge__api__rust_api_clear_contact_request_notifications_impl(port, ptr, rust_vec_len, data_len), +30 => wire__crate__bridge__api__rust_api_clear_conversation_notifications_impl(port, ptr, rust_vec_len, data_len), +31 => wire__crate__bridge__api__rust_api_close_impl(port, ptr, rust_vec_len, data_len), +32 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len), +33 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len), +34 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len), +35 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len), +36 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len), +38 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len), +39 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len), +40 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len), +41 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len), +42 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len), +43 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len), +44 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len), +45 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len), +46 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len), +47 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len), +48 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len), +49 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len), +50 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len), +51 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len), +52 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len), +53 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +54 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len), +55 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len), +56 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len), +57 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len), +58 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len), +59 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len), +60 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len), +61 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len), +62 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len), +63 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len), +64 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len), +65 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len), +66 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len), +67 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len), +68 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len), +69 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len), +70 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len), +71 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len), +72 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len), +73 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len), +74 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len), +75 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len), +76 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len), +77 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len), +78 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len), +79 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len), +80 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len), +81 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len), +82 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len), +83 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len), +84 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len), +85 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len), +86 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len), +87 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len), +88 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len), +89 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len), +90 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len), +91 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len), +92 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len), +93 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len), +94 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len), +95 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len), +96 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len), +97 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len), +98 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len), +99 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len), +100 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len), +101 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len), +102 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len), +103 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len), +104 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len), +105 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len), +106 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len), +107 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len), +108 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len), +109 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len), +110 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len), +111 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len), +112 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len), +113 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len), +114 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +115 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len), +116 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +117 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), +118 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), +119 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +120 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len), +121 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), +122 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +123 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), +124 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +125 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), +126 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), +127 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +128 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len), +129 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len), +130 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), +131 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), +132 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len), +133 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len), +134 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), +135 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), +136 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), +137 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len), +138 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len), +140 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len), +141 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len), +142 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len), +143 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len), +144 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -6527,16 +6642,17 @@ 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 { - 19 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), - 21 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len), - 33 => { + 23 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len), + 25 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len), + 37 => { wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len) } - 136 => wire__crate__bridge__user_config__user_config_api_clone_impl( + 139 => wire__crate__bridge__user_config__user_config_api_clone_impl( ptr, rust_vec_len, data_len, ), + 145 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -6979,6 +7095,30 @@ impl } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::bridge::logging::LogLevel { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Finest => 0.into_dart(), + Self::Fine => 1.into_dart(), + Self::Info => 2.into_dart(), + Self::Warning => 3.into_dart(), + Self::Shout => 4.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::bridge::logging::LogLevel +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::bridge::logging::LogLevel +{ + fn into_into_dart(self) -> crate::bridge::logging::LogLevel { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::services::media_upload::MediaSizeReport { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -7350,7 +7490,6 @@ impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig { .into_into_dart() .into_dart(), self.typing_indicators.into_into_dart().into_dart(), - self.sealed_sender_enabled.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 @@ -7466,13 +7605,6 @@ impl SseEncode for std::collections::HashMap> { } } -impl SseEncode for StreamSink { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - unimplemented!("") - } -} - impl SseEncode for StreamSink { @@ -7882,6 +8014,25 @@ impl SseEncode for Vec { } } +impl SseEncode for crate::bridge::logging::LogLevel { + // 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::bridge::logging::LogLevel::Finest => 0, + crate::bridge::logging::LogLevel::Fine => 1, + crate::bridge::logging::LogLevel::Info => 2, + crate::bridge::logging::LogLevel::Warning => 3, + crate::bridge::logging::LogLevel::Shout => 4, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + impl SseEncode for crate::services::media_upload::MediaSizeReport { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -8275,7 +8426,6 @@ impl SseEncode for crate::user_config::UserConfig { ::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.sealed_sender_enabled, serializer); ::sse_encode(self.show_restore_flame, serializer); >::sse_encode(self.my_best_friend_group_id, serializer); >>::sse_encode( diff --git a/rust/src/lib.rs b/rust/src/lib.rs index fec96ac8..dbea4408 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -14,7 +14,6 @@ mod frb_generated; mod keys; pub mod log; mod native; -pub mod sealed_sender; mod secure_storage; pub mod services; pub mod signal; diff --git a/rust/src/log.rs b/rust/src/log.rs index 504065d0..afcf71d4 100644 --- a/rust/src/log.rs +++ b/rust/src/log.rs @@ -3,26 +3,112 @@ * */ -use crate::bridge::callbacks::{ - get_callbacks, - log::{set_dart_sink, DartWriter}, -}; +use crate::bridge::logging::LogLevel; +use crate::error::{Result, TwonlyError}; use std::fmt; -use std::path::Path; -use std::sync::{Mutex, OnceLock}; +use std::fs::{File, OpenOptions}; +use std::io::{Seek, Write as IoWrite}; +use std::path::{Path, PathBuf}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, OnceLock, +}; use tracing::{Event, Subscriber}; -use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; use tracing_subscriber::{ - fmt::{format::Writer, FmtContext, FormatEvent, FormatFields, FormattedFields, Layer}, + fmt::{ + format::Writer, FmtContext, FormatEvent, FormatFields, FormattedFields, Layer, MakeWriter, + }, layer::SubscriberExt, registry::LookupSpan, util::SubscriberInitExt, EnvFilter, Registry, }; -type TracingGuards = (Option, WorkerGuard); -static TRACING_GUARDS: OnceLock>> = OnceLock::new(); static TRACING_INIT: OnceLock<()> = OnceLock::new(); +static APP_LOG: OnceLock = OnceLock::new(); +static RUST_LOG_IN_BACKGROUND: AtomicBool = AtomicBool::new(false); + +struct AppLog { + path: PathBuf, + file: Mutex, +} + +impl AppLog { + fn open(path: PathBuf) -> std::io::Result { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let file = OpenOptions::new().create(true).append(true).open(&path)?; + Ok(Self { + path, + file: Mutex::new(file), + }) + } + + fn append(&self, bytes: &[u8]) -> std::io::Result<()> { + let mut file = self + .file + .lock() + .map_err(|_| std::io::Error::other("app.log lock was poisoned"))?; + file.write_all(bytes) + } + + fn read(&self) -> std::io::Result { + let _file = self + .file + .lock() + .map_err(|_| std::io::Error::other("app.log lock was poisoned"))?; + std::fs::read_to_string(&self.path) + } + + fn replace(&self, contents: &str) -> std::io::Result<()> { + let mut file = self + .file + .lock() + .map_err(|_| std::io::Error::other("app.log lock was poisoned"))?; + file.set_len(0)?; + file.seek(std::io::SeekFrom::Start(0))?; + file.write_all(contents.as_bytes())?; + file.seek(std::io::SeekFrom::End(0))?; + Ok(()) + } +} + +#[derive(Clone, Copy)] +struct AppLogWriter; + +struct AppLogBuffer(Vec); + +impl std::io::Write for AppLogBuffer { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl Drop for AppLogBuffer { + fn drop(&mut self) { + if !self.0.is_empty() { + if let Some(log) = APP_LOG.get() { + if let Err(error) = log.append(&self.0) { + eprintln!("Failed to append to app.log: {error}"); + } + } + } + } +} + +impl<'a> MakeWriter<'a> for AppLogWriter { + type Writer = AppLogBuffer; + + fn make_writer(&'a self) -> Self::Writer { + AppLogBuffer(Vec::new()) + } +} #[derive(Clone, Copy, Debug, Default)] pub struct PlainFields; @@ -47,6 +133,7 @@ impl ShortEventFormatter { Self { ansi: true } } + /// The same format without escape codes, for a sink that is not a terminal. pub const fn plain() -> Self { Self { ansi: false } } @@ -136,49 +223,99 @@ where } } -pub(crate) async fn init_tracing(logs_dir: &std::path::Path, is_dart_available: bool) { - let _ = std::fs::create_dir_all(logs_dir); +#[derive(Clone, Copy, Debug, Default)] +struct AppLogEventFormatter; - // Runs on *every* init, not just the first one: the subscriber is installed - // once per process, but the isolate it logs into can be replaced (hot - // restart, engine restart, background isolate). Re-registering here points - // the already-installed Dart layer at the isolate that is alive now. - if is_dart_available { - if let Ok(callbacks) = get_callbacks() { - set_dart_sink((callbacks.logging.get_stream_sink)().await); +impl FormatEvent for AppLogEventFormatter +where + S: Subscriber + for<'lookup> LookupSpan<'lookup>, + N: for<'writer> FormatFields<'writer> + 'static, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + let metadata = event.metadata(); + let source = metadata + .file() + .map(Path::new) + .and_then(|path| { + let parent = path.parent()?.file_name()?; + let file = path.file_name()?; + Some(format!( + "{}/{}:{}", + parent.to_string_lossy(), + file.to_string_lossy(), + metadata.line().unwrap_or_default() + )) + }) + .unwrap_or_else(|| metadata.target().to_owned()); + let level = match *metadata.level() { + tracing::Level::TRACE => "FINEST", + tracing::Level::DEBUG => "FINE", + tracing::Level::INFO => "INFO", + tracing::Level::WARN => "WARNING", + tracing::Level::ERROR => "SHOUT", + }; + let task = if RUST_LOG_IN_BACKGROUND.load(Ordering::Relaxed) { + 'b' + } else { + 'f' + }; + + write!( + writer, + "{} {level} [{task}] [twonly] {source} > ", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.6f") + )?; + + if let Some(scope) = ctx.event_scope() { + for span in scope.from_root() { + let extensions = span.extensions(); + if let Some(fields) = extensions.get::>() { + if !fields.is_empty() { + write!(writer, "{fields} ")?; + } + } + } + } + + ctx.field_format().format_fields(writer.by_ref(), event)?; + writeln!(writer) + } +} + +pub(crate) fn init_tracing(data_dir: &Path, in_background: bool) { + RUST_LOG_IN_BACKGROUND.store(in_background, Ordering::Relaxed); + + if APP_LOG.get().is_none() { + match AppLog::open(data_dir.join("app.log")) { + Ok(log) => { + let _ = APP_LOG.set(log); + } + Err(error) => eprintln!("Failed to open app.log: {error}"), } } TRACING_INIT.get_or_init(|| { - let (non_blocking_stdout, _non_blocking_file) = build_writers(logs_dir); - let stdout_layer = Layer::new() - .with_writer(non_blocking_stdout) + .with_writer(std::io::stdout) .with_ansi(false) .event_format(ShortEventFormatter::ansi()); - // let file_layer = Layer::new() - // .with_writer(non_blocking_file) - // .with_ansi(false) - // .with_target(true); - - // Replace stdout with our new DartWriter! - let default_filter = if std::env::var("FLUTTER_TEST").is_ok() { "info,refinery_core=warn,refinery=warn" } else { "debug,refinery_core=warn,refinery=warn" }; - // DartWriter resolves the current sink per write, so the layer is - // installed unconditionally -- it is simply a no-op until an isolate - // registers one. PlainFields separates Dart span fields from stdout fields, - // preventing duplicate fields across layers and keeping Dart fields plain. - let dart_layer = Layer::new() - .with_writer(DartWriter) + let file_layer = Layer::new() + .with_writer(AppLogWriter) .with_ansi(false) .fmt_fields(PlainFields) - .event_format(ShortEventFormatter::plain()); + .event_format(AppLogEventFormatter); let _ = Registry::default() .with( @@ -186,37 +323,103 @@ pub(crate) async fn init_tracing(logs_dir: &std::path::Path, is_dart_available: .unwrap_or_else(|_| EnvFilter::new(default_filter)), ) .with(stdout_layer) - .with(dart_layer) + .with(file_layer) .try_init(); }); } -fn build_writers(logs_dir: &std::path::Path) -> (NonBlocking, NonBlocking) { - let file_appender_res = tracing_appender::rolling::RollingFileAppender::builder() - .rotation(tracing_appender::rolling::Rotation::DAILY) - .filename_prefix("twonly") - .filename_suffix("log") - .build(logs_dir); - - let (non_blocking_file, file_guard) = match file_appender_res { - Ok(file_appender) => { - let (nb, guard) = tracing_appender::non_blocking(file_appender); - (nb, Some(guard)) - } - Err(e) => { - eprintln!("Failed to create file appender: {}", e); - let (nb, _guard) = tracing_appender::non_blocking(std::io::sink()); - (nb, None) - } - }; - let (non_blocking_stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout()); - - // The stdout guard must outlive this function regardless of whether the - // file appender came up -- dropping it shuts the non-blocking writer thread - // down and silently swallows every log line. - TRACING_GUARDS - .set(Mutex::new(Some((file_guard, stdout_guard)))) - .ok(); - - (non_blocking_stdout, non_blocking_file) +fn app_log() -> Result<&'static AppLog> { + APP_LOG.get().ok_or(TwonlyError::Initialization) +} + +pub(crate) fn write_dart_log( + level: LogLevel, + source: &str, + message: &str, + in_background: bool, +) -> Result<()> { + let log = app_log()?; + let level = match level { + LogLevel::Finest => "FINEST", + LogLevel::Fine => "FINE", + LogLevel::Info => "INFO", + LogLevel::Warning => "WARNING", + LogLevel::Shout => "SHOUT", + }; + let task = if in_background { 'b' } else { 'f' }; + let line = format!( + "{} {level} [{task}] [twonly] {source} > {message}\n", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.6f") + ); + log.append(line.as_bytes())?; + Ok(()) +} + +pub(crate) fn load_log_file() -> Result { + Ok(app_log()?.read()?) +} + +pub(crate) fn read_last_log_lines(line_count: usize) -> Result { + let contents = app_log()?.read()?; + let lines: Vec<_> = contents.lines().collect(); + let start = lines.len().saturating_sub(line_count); + Ok(lines[start..].join("\n")) +} + +pub(crate) fn clean_log_file() -> Result<()> { + let log = app_log()?; + let contents = log.read()?; + let cutoff = chrono::Local::now().naive_local() - chrono::Duration::days(3); + let keep_from = contents.lines().position(|line| { + line.get(..26) + .and_then(|timestamp| { + chrono::NaiveDateTime::parse_from_str(timestamp, "%Y-%m-%d %H:%M:%S%.f").ok() + }) + .is_some_and(|timestamp| timestamp > cutoff) + }); + + let replacement = match keep_from { + Some(0) => return Ok(()), + Some(index) => { + let mut retained = contents.lines().skip(index).collect::>().join("\n"); + if !retained.is_empty() { + retained.push('\n'); + } + retained + } + None => String::new(), + }; + log.replace(&replacement)?; + Ok(()) +} + +pub(crate) fn clear_log_file() -> Result { + let log = app_log()?; + let had_contents = log + .path + .metadata() + .map(|metadata| metadata.len() > 0) + .unwrap_or(false); + log.replace("")?; + Ok(had_contents) +} + +#[cfg(test)] +mod tests { + use super::AppLog; + + #[test] + fn app_log_serializes_reads_and_truncation() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("app.log"); + let log = AppLog::open(path.clone()).unwrap(); + + log.append(b"first\n").unwrap(); + log.append(b"second\n").unwrap(); + assert_eq!(log.read().unwrap(), "first\nsecond\n"); + + log.replace("retained\n").unwrap(); + log.append(b"new\n").unwrap(); + assert_eq!(std::fs::read_to_string(path).unwrap(), "retained\nnew\n"); + } } diff --git a/rust/src/sealed_sender.rs b/rust/src/sealed_sender.rs deleted file mode 100644 index 0cac4a4b..00000000 --- a/rust/src/sealed_sender.rs +++ /dev/null @@ -1,531 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -use chacha20poly1305::{ - aead::{Aead, Payload}, - KeyInit, XChaCha20Poly1305, XNonce, -}; -use hkdf::Hkdf; -use libsignal_protocol::{IdentityKeyPair, KeyPair, PublicKey}; -use prost::Message as ProstMessage; -use rand::{CryptoRng, Rng}; -use sha2::Sha256; -use std::time::{SystemTime, UNIX_EPOCH}; -use thiserror::Error; - -use crate::api::proto::client as proto; -use crate::context::Context; - -const PAYLOAD_MAGIC: &str = "twonly-message-envelope-v1"; -const ENCRYPTION_CONTEXT: &[u8] = b"twonly-message-envelope-encryption-v1"; -const X25519_PUBLIC_KEY_SIZE: usize = 32; -const XCHACHA20_NONCE_SIZE: usize = 24; -const POLY1305_TAG_SIZE: usize = 16; -const ENVELOPE_VALIDITY_SECONDS: i64 = 45 * 24 * 60 * 60; -const MAX_FUTURE_CLOCK_SKEW_SECONDS: i64 = 5 * 60; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum SealedSenderError { - #[error("invalid protobuf: {0}")] - InvalidProtobuf(#[from] prost::DecodeError), - #[error("invalid ephemeral public key")] - InvalidEphemeralPublicKey, - #[error("invalid XChaCha20-Poly1305 nonce")] - InvalidNonce, - #[error("invalid XChaCha20-Poly1305 ciphertext")] - InvalidCiphertext, - #[error("X25519 key agreement failed")] - KeyAgreement, - #[error("sender signature generation failed")] - SignatureGeneration, - #[error("HKDF key derivation failed")] - KeyDerivation, - #[error("XChaCha20-Poly1305 encryption failed")] - EncryptionFailed, - #[error("XChaCha20-Poly1305 authentication failed")] - AuthenticationFailed, - #[error("invalid sealed-sender payload magic")] - InvalidMagic, - #[error("message was addressed to user {actual}, not user {expected}")] - WrongRecipient { expected: i64, actual: i64 }, - #[error("sealed-sender payload does not contain a message")] - MissingMessage, - #[error("sealed-sender message has an empty receipt ID")] - MissingReceiptId, - #[error("no identity key is available for sender {0}")] - UnknownSender(i64), - #[error("invalid sender signature")] - InvalidSignature, - #[error("sealed-sender context error: {0}")] - Context(String), - #[error("the local user ID is unavailable")] - MissingLocalUserId, - #[error("the local Signal identity is unavailable")] - MissingLocalIdentity, - #[error("invalid local Signal identity")] - InvalidLocalIdentity, - #[error("system clock is before the Unix epoch")] - InvalidSystemTime, - #[error("sealed-sender envelope has expired")] - ExpiredEnvelope, - #[error("sealed-sender envelope timestamp is too far in the future")] - FutureEnvelope, -} - -/// Encrypts and decrypts twonly sealed-sender envelopes. -/// -/// The outer encryption authenticates the ciphertext but intentionally does not -/// authenticate the sender. Sender authentication is provided by the XEdDSA -/// identity-key signature inside the encrypted envelope. -pub(crate) struct SealedSender; - -impl SealedSender { - pub(crate) fn encrypt( - from_user_id: i64, - recipient_user_id: i64, - message: proto::Message, - sender_identity: &IdentityKeyPair, - recipient_identity_key: &PublicKey, - rng: &mut R, - ) -> Result> - where - R: Rng + CryptoRng + ?Sized, - { - Self::encrypt_at( - from_user_id, - recipient_user_id, - message, - unix_time_seconds()?, - sender_identity, - recipient_identity_key, - rng, - ) - } - - fn encrypt_at( - from_user_id: i64, - recipient_user_id: i64, - message: proto::Message, - created_at_unix_seconds: i64, - sender_identity: &IdentityKeyPair, - recipient_identity_key: &PublicKey, - rng: &mut R, - ) -> Result> - where - R: Rng + CryptoRng + ?Sized, - { - if message.receipt_id.is_empty() { - return Err(SealedSenderError::MissingReceiptId); - } - let payload = proto::MessageEnvelopePayload { - magic: PAYLOAD_MAGIC.to_owned(), - from_user_id, - recipient_user_id, - message: Some(message), - created_at_unix_seconds, - }; - let signed_payload = payload.encode_to_vec(); - let signature = sender_identity - .private_key() - .calculate_signature(&signed_payload, rng) - .map_err(|_| SealedSenderError::SignatureGeneration)?; - let envelope = proto::MessageEnvelope { - signed_payload, - signature: signature.into_vec(), - } - .encode_to_vec(); - - let ephemeral_key_pair = KeyPair::generate(rng); - let ephemeral_public_key = ephemeral_key_pair.public_key.public_key_bytes().to_vec(); - let shared_secret = ephemeral_key_pair - .private_key - .calculate_agreement(recipient_identity_key) - .map_err(|_| SealedSenderError::KeyAgreement)?; - let key = derive_encryption_key( - &shared_secret, - &ephemeral_public_key, - recipient_identity_key, - )?; - - let mut nonce = [0_u8; XCHACHA20_NONCE_SIZE]; - rng.fill_bytes(&mut nonce); - let ciphertext = XChaCha20Poly1305::new((&key).into()) - .encrypt( - XNonce::from_slice(&nonce), - Payload { - msg: &envelope, - aad: ENCRYPTION_CONTEXT, - }, - ) - .map_err(|_| SealedSenderError::EncryptionFailed)?; - - Ok(proto::EncryptedMessageEnvelope { - ephemeral_public_key, - nonce: nonce.to_vec(), - ciphertext, - } - .encode_to_vec()) - } - - /// Decrypts an envelope with the local identity from `context` and verifies - /// the signature using the sender identity stored by Signal. - pub(crate) async fn decrypt( - encrypted_envelope: &[u8], - context: &Context, - ) -> Result { - let (recipient_user_id, recipient_identity) = { - let key_manager = context.key_manager.lock().await; - let recipient_user_id = key_manager - .user_id - .ok_or(SealedSenderError::MissingLocalUserId)?; - let serialized_identity = &key_manager - .signal_identity - .as_ref() - .ok_or(SealedSenderError::MissingLocalIdentity)? - .identity_key_pair_structure; - let recipient_identity = IdentityKeyPair::try_from(serialized_identity.as_slice()) - .map_err(|_| SealedSenderError::InvalidLocalIdentity)?; - (recipient_user_id, recipient_identity) - }; - - let (envelope, payload) = - Self::decrypt_envelope(encrypted_envelope, recipient_user_id, &recipient_identity)?; - let sender_identity = context - .get_identity(payload.from_user_id) - .await - .map_err(context_error)? - .ok_or(SealedSenderError::UnknownSender(payload.from_user_id))?; - verify_sender_and_message(&envelope, &payload, sender_identity.public_key())?; - Ok(payload) - } - - fn decrypt_envelope( - encrypted_envelope: &[u8], - recipient_user_id: i64, - recipient_identity: &IdentityKeyPair, - ) -> Result<(proto::MessageEnvelope, proto::MessageEnvelopePayload)> { - let encrypted = proto::EncryptedMessageEnvelope::decode(encrypted_envelope)?; - if encrypted.ephemeral_public_key.len() != X25519_PUBLIC_KEY_SIZE { - return Err(SealedSenderError::InvalidEphemeralPublicKey); - } - if encrypted.nonce.len() != XCHACHA20_NONCE_SIZE { - return Err(SealedSenderError::InvalidNonce); - } - if encrypted.ciphertext.len() < POLY1305_TAG_SIZE { - return Err(SealedSenderError::InvalidCiphertext); - } - - let ephemeral_public_key = - PublicKey::from_djb_public_key_bytes(&encrypted.ephemeral_public_key) - .map_err(|_| SealedSenderError::InvalidEphemeralPublicKey)?; - let shared_secret = recipient_identity - .private_key() - .calculate_agreement(&ephemeral_public_key) - .map_err(|_| SealedSenderError::KeyAgreement)?; - let key = derive_encryption_key( - &shared_secret, - &encrypted.ephemeral_public_key, - recipient_identity.identity_key().public_key(), - )?; - let plaintext = XChaCha20Poly1305::new((&key).into()) - .decrypt( - XNonce::from_slice(&encrypted.nonce), - Payload { - msg: &encrypted.ciphertext, - aad: ENCRYPTION_CONTEXT, - }, - ) - .map_err(|_| SealedSenderError::AuthenticationFailed)?; - - let envelope = proto::MessageEnvelope::decode(plaintext.as_slice())?; - let payload = proto::MessageEnvelopePayload::decode(envelope.signed_payload.as_slice())?; - if payload.magic != PAYLOAD_MAGIC { - return Err(SealedSenderError::InvalidMagic); - } - if payload.recipient_user_id != recipient_user_id { - return Err(SealedSenderError::WrongRecipient { - expected: recipient_user_id, - actual: payload.recipient_user_id, - }); - } - Ok((envelope, payload)) - } -} - -fn verify_sender_and_message( - envelope: &proto::MessageEnvelope, - payload: &proto::MessageEnvelopePayload, - sender_key: &PublicKey, -) -> Result<()> { - if !sender_key.verify_signature(&envelope.signed_payload, &envelope.signature) { - return Err(SealedSenderError::InvalidSignature); - } - let message = payload - .message - .as_ref() - .ok_or(SealedSenderError::MissingMessage)?; - if message.receipt_id.is_empty() { - return Err(SealedSenderError::MissingReceiptId); - } - let now = unix_time_seconds()?; - if payload.created_at_unix_seconds < now.saturating_sub(ENVELOPE_VALIDITY_SECONDS) { - return Err(SealedSenderError::ExpiredEnvelope); - } - if payload.created_at_unix_seconds > now.saturating_add(MAX_FUTURE_CLOCK_SKEW_SECONDS) { - return Err(SealedSenderError::FutureEnvelope); - } - Ok(()) -} - -fn context_error(error: impl std::fmt::Display) -> SealedSenderError { - SealedSenderError::Context(error.to_string()) -} - -fn unix_time_seconds() -> Result { - let seconds = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| SealedSenderError::InvalidSystemTime)? - .as_secs(); - i64::try_from(seconds).map_err(|_| SealedSenderError::InvalidSystemTime) -} - -fn derive_encryption_key( - shared_secret: &[u8], - ephemeral_public_key: &[u8], - recipient_identity_key: &PublicKey, -) -> Result<[u8; 32]> { - let mut info = Vec::with_capacity( - ENCRYPTION_CONTEXT.len() - + ephemeral_public_key.len() - + recipient_identity_key.serialize().len(), - ); - info.extend_from_slice(ENCRYPTION_CONTEXT); - info.extend_from_slice(ephemeral_public_key); - info.extend_from_slice(&recipient_identity_key.serialize()); - - let mut key = [0_u8; 32]; - Hkdf::::new(None, shared_secret) - .expand(&info, &mut key) - .map_err(|_| SealedSenderError::KeyDerivation)?; - Ok(key) -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::{rngs::StdRng, SeedableRng}; - - fn test_message() -> proto::Message { - proto::Message { - r#type: proto::message::Type::PlaintextContent as i32, - receipt_id: "1f58417a-5cd4-4bb6-9f38-2f499d9cfa8e".to_owned(), - ..Default::default() - } - } - - fn decrypt_with_identity( - encrypted: &[u8], - recipient_user_id: i64, - recipient: &IdentityKeyPair, - sender_key: &PublicKey, - ) -> Result { - let (envelope, payload) = - SealedSender::decrypt_envelope(encrypted, recipient_user_id, recipient)?; - verify_sender_and_message(&envelope, &payload, sender_key)?; - Ok(payload) - } - - #[tokio::test] - async fn encrypts_decrypts_and_authenticates_a_sealed_sender_message() { - let mut rng = StdRng::seed_from_u64(7); - let sender = IdentityKeyPair::generate(&mut rng); - let recipient = IdentityKeyPair::generate(&mut rng); - let message = test_message(); - let temporary_directory = tempfile::tempdir().unwrap(); - let context = Context::init_for_testing( - temporary_directory.path().join("database"), - temporary_directory.path().join("data"), - ) - .await - .unwrap(); - { - let mut key_manager = context.key_manager.lock().await; - key_manager.user_id = Some(42); - key_manager.signal_identity = Some(crate::keys::SignalIdentityKey { - identity_key_pair_structure: recipient.serialize().to_vec(), - registration_id: 1, - }); - } - let database = context.rust_db.read().await.clone(); - let sender_identity = sender.identity_key().serialize(); - sqlx::query!( - r#" - INSERT INTO signal_identities (name, identity_key, timestamp) - VALUES (?, ?, 0) - "#, - "41", - sender_identity.as_ref(), - ) - .execute(&database.pool) - .await - .unwrap(); - - let encrypted = SealedSender::encrypt( - 41, - 42, - message.clone(), - &sender, - recipient.identity_key().public_key(), - &mut rng, - ) - .unwrap(); - let decrypted = SealedSender::decrypt(&encrypted, &context).await.unwrap(); - - assert_eq!(decrypted.magic, PAYLOAD_MAGIC); - assert_eq!(decrypted.from_user_id, 41); - assert_eq!(decrypted.recipient_user_id, 42); - assert_eq!(decrypted.message, Some(message)); - assert!(decrypted.created_at_unix_seconds > 0); - } - - #[test] - fn rejects_tampered_ciphertext() { - let mut rng = StdRng::seed_from_u64(8); - let sender = IdentityKeyPair::generate(&mut rng); - let recipient = IdentityKeyPair::generate(&mut rng); - let encrypted = SealedSender::encrypt( - 41, - 42, - test_message(), - &sender, - recipient.identity_key().public_key(), - &mut rng, - ) - .unwrap(); - let mut wrapper = proto::EncryptedMessageEnvelope::decode(encrypted.as_slice()).unwrap(); - wrapper.ciphertext[0] ^= 1; - - let error = decrypt_with_identity( - &wrapper.encode_to_vec(), - 42, - &recipient, - sender.identity_key().public_key(), - ) - .unwrap_err(); - - assert!(matches!(error, SealedSenderError::AuthenticationFailed)); - } - - #[test] - fn rejects_a_signature_from_another_identity() { - let mut rng = StdRng::seed_from_u64(9); - let sender = IdentityKeyPair::generate(&mut rng); - let impostor = IdentityKeyPair::generate(&mut rng); - let recipient = IdentityKeyPair::generate(&mut rng); - let encrypted = SealedSender::encrypt( - 41, - 42, - test_message(), - &sender, - recipient.identity_key().public_key(), - &mut rng, - ) - .unwrap(); - - let error = decrypt_with_identity( - &encrypted, - 42, - &recipient, - impostor.identity_key().public_key(), - ) - .unwrap_err(); - - assert!(matches!(error, SealedSenderError::InvalidSignature)); - } - - #[test] - fn rejects_an_envelope_for_another_recipient_before_resolving_sender() { - let mut rng = StdRng::seed_from_u64(10); - let sender = IdentityKeyPair::generate(&mut rng); - let recipient = IdentityKeyPair::generate(&mut rng); - let encrypted = SealedSender::encrypt( - 41, - 42, - test_message(), - &sender, - recipient.identity_key().public_key(), - &mut rng, - ) - .unwrap(); - - let error = SealedSender::decrypt_envelope(&encrypted, 43, &recipient).unwrap_err(); - - assert!(matches!( - error, - SealedSenderError::WrongRecipient { - expected: 43, - actual: 42 - } - )); - } - - #[test] - fn rejects_an_expired_envelope() { - let mut rng = StdRng::seed_from_u64(11); - let sender = IdentityKeyPair::generate(&mut rng); - let recipient = IdentityKeyPair::generate(&mut rng); - let expired_at = unix_time_seconds().unwrap() - ENVELOPE_VALIDITY_SECONDS - 1; - let encrypted = SealedSender::encrypt_at( - 41, - 42, - test_message(), - expired_at, - &sender, - recipient.identity_key().public_key(), - &mut rng, - ) - .unwrap(); - - let error = decrypt_with_identity( - &encrypted, - 42, - &recipient, - sender.identity_key().public_key(), - ) - .unwrap_err(); - - assert!(matches!(error, SealedSenderError::ExpiredEnvelope)); - } - - #[test] - fn rejects_an_envelope_too_far_in_the_future() { - let mut rng = StdRng::seed_from_u64(12); - let sender = IdentityKeyPair::generate(&mut rng); - let recipient = IdentityKeyPair::generate(&mut rng); - let future_at = unix_time_seconds().unwrap() + MAX_FUTURE_CLOCK_SKEW_SECONDS + 1; - let encrypted = SealedSender::encrypt_at( - 41, - 42, - test_message(), - future_at, - &sender, - recipient.identity_key().public_key(), - &mut rng, - ) - .unwrap(); - - let error = decrypt_with_identity( - &encrypted, - 42, - &recipient, - sender.identity_key().public_key(), - ) - .unwrap_err(); - - assert!(matches!(error, SealedSenderError::FutureEnvelope)); - } -} diff --git a/rust/src/services/media_upload.rs b/rust/src/services/media_upload.rs index 58c47fc2..c1eb43df 100644 --- a/rust/src/services/media_upload.rs +++ b/rust/src/services/media_upload.rs @@ -249,6 +249,12 @@ impl MediaUploadService { if !temp_path.exists() { return self.retire_media(&media).await; } + // What the recipient ends up with is this plaintext, so its size is + // recorded while the file is still here: the temporary copy is dropped + // once the upload is scheduled, and the message info still shows it. + if let Err(error) = self.record_plaintext_size(media_id, &temp_path).await { + tracing::warn!(media_id, %error, "could not record the media size"); + } // Auto-storing has to happen before the plaintext is consumed, and only // for media the recipient could have kept anyway. @@ -571,6 +577,19 @@ impl MediaUploadService { Ok(()) } + /// The size the user is shown for a media file is the plaintext one, not + /// what the encrypted upload weighs, and it is kept even after the file + /// itself is gone. + async fn record_plaintext_size(&self, media_id: &str, path: &Path) -> Result<()> { + let size = std::fs::metadata(path)?.len() as i64; + self.update_media( + "UPDATE media_files SET size_in_bytes = ? WHERE media_id = ?", + size, + media_id, + ) + .await + } + /// Called by Flutter after a plugin step rewrote a media file on disk, so /// the derived state Rust owns is recomputed from what was actually written. pub async fn media_step_finished(&self, media_id: &str, kind: &str) -> Result<()> { diff --git a/rust/src/services/mediafiles.rs b/rust/src/services/mediafiles.rs index fe71ddb5..4deef12b 100644 --- a/rust/src/services/mediafiles.rs +++ b/rust/src/services/mediafiles.rs @@ -263,14 +263,17 @@ impl MediaFileService { std::fs::write(&temp_path, &bytes)?; let hash = Sha256::digest(&bytes).to_vec(); + let size = bytes.len() as i64; // Keep the file update and state transition ordered: ready is only // visible after the plaintext has been written successfully. let database = self.ctx.app_db.read().await.clone(); sqlx::query!( - r#"UPDATE media_files SET download_state = 'ready', stored_file_hash = ? + r#"UPDATE media_files SET download_state = 'ready', stored_file_hash = ?, + size_in_bytes = ? WHERE media_id = ?"#, hash, + size, media.media_id, ) .execute(&database.pool) diff --git a/rust/src/services/mod.rs b/rust/src/services/mod.rs index 324aa577..6b7af4f9 100644 --- a/rust/src/services/mod.rs +++ b/rust/src/services/mod.rs @@ -13,5 +13,3 @@ pub mod media_upload; pub mod mediafiles; pub mod messages; pub mod notifications; -pub mod privacy_pass; -pub mod sealed_sender; diff --git a/rust/src/services/notifications.rs b/rust/src/services/notifications.rs index 71fa4d14..86a072a5 100644 --- a/rust/src/services/notifications.rs +++ b/rust/src/services/notifications.rs @@ -95,7 +95,7 @@ pub struct NotificationPresentation { pub fn fallback_presentation(locale: &str) -> NotificationPresentation { NotificationPresentation { title: translation(locale, "notificationCategoryMessageTitle").to_owned(), - body: translation(locale, "notificationCategoryMessageDesc").to_owned(), + body: translation(locale, "notificationConnectionFallback").to_owned(), } } @@ -692,6 +692,18 @@ mod tests { assert_eq!(localized_body("fr-FR", &row), "wants to connect with you."); } + #[test] + fn localizes_connection_fallback_notification() { + assert_eq!( + fallback_presentation("en-US").body, + "You may have new messages." + ); + assert_eq!( + fallback_presentation("de-DE").body, + "Du könntest neue Nachrichten haben." + ); + } + #[test] fn interpolates_group_and_reaction_placeholders() { let mut group = pending_row("text"); diff --git a/rust/src/services/privacy_pass.rs b/rust/src/services/privacy_pass.rs deleted file mode 100644 index 8159c5a9..00000000 --- a/rust/src/services/privacy_pass.rs +++ /dev/null @@ -1,364 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -//! Client side of the Privacy Pass token pool that rate-limits sealed-sender -//! uploads. -//! -//! Tokens are minted over the authenticated WebSocket, where the server counts -//! them against this account's daily quota, and spent later on the anonymous -//! HTTP upload endpoint. Because the issued token is unblinded locally, the -//! server cannot link a redeemed token back to the account it was issued to, -//! which is what lets the upload stay unauthenticated without becoming an open -//! relay. - -use crate::api::Server; -use crate::bridge::api::ServerResult; -use crate::context::Context; -use crate::error::{Result, TwonlyError}; -use crate::user_config::UserConfig; -use p384::NistP384; -use privacypass::auth::authenticate::TokenChallenge; -use privacypass::common::private::deserialize_public_key; -use privacypass::private_tokens::{TokenRequest, TokenResponse, TokenState}; -use privacypass::Serialize as PrivacyPassSerialize; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -/// Refill once the pool can no longer cover a short burst of messages. -/// -/// The issuer only lets a session mint every few seconds, so this has to leave -/// room for a whole cooldown's worth of sends: a threshold that trips only once -/// the pool is nearly empty guarantees a stretch of named messages while the -/// refill waits its turn. -const REFILL_THRESHOLD: i64 = 10; -/// Never ask for more than this, however large the server's batch limit is. -/// Every token in a batch costs a blind and an unblind, both P-384 scalar -/// multiplications, so the batch size is what bounds the CPU spike a refill -/// puts on the device. -const MAX_TOKENS_PER_REFILL: usize = 20; -/// Stop using a token slightly before the server would reject it, so a message -/// in flight around midnight UTC is not refused. -const EXPIRY_SAFETY_MARGIN_SECONDS: i64 = 5 * 60; -/// Back off this long when the issuer refuses for a reason we cannot classify. -const UNKNOWN_REFUSAL_BACKOFF: Duration = Duration::from_secs(15 * 60); -/// Assumed issuance cooldown while the server's own value is unknown, which is -/// the case only when the parameter request itself failed. -const ASSUMED_ISSUANCE_COOLDOWN: Duration = Duration::from_secs(5); -/// A refill waits out a backoff no longer than this and gives up on anything -/// beyond it. The issuance cooldown is seconds long and worth waiting for; an -/// exhausted daily quota lasts until midnight and is not. -const MAX_BACKOFF_WAIT: Duration = Duration::from_secs(30); - -pub(crate) struct PrivacyPassTokens; - -impl PrivacyPassTokens { - /// Removes one unexpired token from the local pool. - /// - /// A token is spent by handing it to the server, so it is deleted before it - /// is used: replaying it would be rejected anyway, and keeping it would - /// stall every later message behind the same dead token. - /// - /// Minting never happens here. It needs two server round trips, and putting - /// those on the send path would delay the very message that emptied the - /// pool; an empty pool simply means this message goes out named while the - /// refill runs behind it. - pub(crate) async fn take(ctx: &Arc) -> Result>> { - let token = Self::pop(ctx).await?; - Self::spawn_refill(ctx); - Ok(token) - } - - /// Tops the pool up when it is running low. Safe to call on every - /// connection: it is a no-op while enough tokens are left. - pub(crate) async fn refill_if_needed(ctx: &Arc) -> Result<()> { - // Tokens are only ever spent on sealed-sender uploads. Minting them for - // an account that has the feature off would burn its daily quota on - // tokens it can never use. - if !UserConfig::load_from(ctx)?.is_some_and(|config| config.sealed_sender_enabled) { - return Ok(()); - } - if Self::count(ctx).await? >= REFILL_THRESHOLD { - return Ok(()); - } - Self::refill(ctx).await - } - - pub(crate) fn spawn_refill(ctx: &Arc) { - let ctx = ctx.clone(); - tokio::spawn(async move { - if let Err(error) = Self::refill_if_needed(&ctx).await { - tracing::warn!("Privacy Pass refill failed: {error}"); - } - }); - } - - async fn count(ctx: &Arc) -> Result { - let database = ctx.app_db.read().await.clone(); - let now = now_seconds(); - Ok(sqlx::query_scalar!( - "SELECT COUNT(*) FROM privacy_pass_tokens WHERE expires_at > ?", - now - ) - .fetch_one(&database.pool) - .await?) - } - - async fn pop(ctx: &Arc) -> Result>> { - let database = ctx.app_db.read().await.clone(); - let now = now_seconds(); - let mut transaction = database.pool.begin().await?; - sqlx::query!("DELETE FROM privacy_pass_tokens WHERE expires_at <= ?", now) - .execute(&mut *transaction) - .await?; - let token = sqlx::query_scalar!( - r#"DELETE FROM privacy_pass_tokens - WHERE token = ( - SELECT token FROM privacy_pass_tokens - WHERE expires_at > ? - ORDER BY expires_at ASC - LIMIT 1 - ) - RETURNING token"#, - now - ) - .fetch_optional(&mut *transaction) - .await?; - transaction.commit().await?; - Ok(token) - } - - async fn refill(ctx: &Arc) -> Result<()> { - // A refill already in flight will fill the pool for everyone waiting on - // it, so a second one would only spend quota twice. - let Ok(mut issuance) = ctx.privacy_pass_issuance.try_lock() else { - return Ok(()); - }; - // A backoff short enough to be the issuer's cooldown is waited out - // rather than skipped: returning here would leave the pool empty with - // nothing scheduled to fill it, so every message until the next send - // would go named. Longer backoffs are not worth holding a task for. - if let Some(next) = *issuance { - let wait = next.saturating_duration_since(Instant::now()); - if wait > MAX_BACKOFF_WAIT { - return Ok(()); - } - if !wait.is_zero() { - tokio::time::sleep(wait).await; - } - } - // Another caller may have filled the pool while this one waited. - if Self::count(ctx).await? >= REFILL_THRESHOLD { - return Ok(()); - } - - let parameters = match Server::get_privacy_pass_parameters(ctx).await? { - ServerResult::Ok(parameters) => parameters, - ServerResult::ErrorCode(code) => { - *issuance = Some(Instant::now() + backoff_for(code, ASSUMED_ISSUANCE_COOLDOWN)); - return Err(TwonlyError::Generic(format!( - "server rejected the Privacy Pass parameter request with code {code}" - ))); - } - }; - - let challenge = TokenChallenge::deserialize(parameters.token_challenge.as_slice()) - .map_err(|error| { - TwonlyError::Generic(format!("invalid Privacy Pass challenge: {error}")) - })?; - let public_key = - deserialize_public_key::(¶meters.public_key).map_err(|error| { - TwonlyError::Generic(format!("invalid Privacy Pass public key: {error}")) - })?; - - let batch_size = (parameters.max_batch_size as usize).min(MAX_TOKENS_PER_REFILL); - if batch_size == 0 { - return Err(TwonlyError::Generic( - "server does not issue any Privacy Pass tokens".into(), - )); - } - - // Blinding a batch is a run of P-384 scalar multiplications. On the - // async runtime it would stall the socket tasks sharing those threads, - // which is felt as dropped connections rather than as slow minting. - let (requests, states) = tokio::task::spawn_blocking(move || { - let mut requests = Vec::with_capacity(batch_size); - let mut states: Vec> = Vec::with_capacity(batch_size); - for _ in 0..batch_size { - let (request, state) = TokenRequest::::new(public_key, &challenge) - .map_err(|error| { - TwonlyError::Generic(format!( - "could not blind a Privacy Pass token: {error}" - )) - })?; - requests.push(request.tls_serialize_detached().map_err(|error| { - TwonlyError::Generic(format!("could not serialize a token request: {error}")) - })?); - states.push(state); - } - Ok::<_, TwonlyError>((requests, states)) - }) - .await - .map_err(|error| TwonlyError::Generic(format!("token blinding panicked: {error}")))??; - - let cooldown = Duration::from_secs(u64::from(parameters.issuance_cooldown_seconds)); - let responses = match Server::issue_privacy_pass_tokens(ctx, requests).await? { - ServerResult::Ok(responses) => { - // The issuer refuses a session that mints again inside its - // cooldown, and that refusal is indistinguishable from real - // trouble. Pacing the next refill here keeps the client from - // provoking one. - *issuance = Some(Instant::now() + cooldown); - responses - } - ServerResult::ErrorCode(code) => { - *issuance = Some(Instant::now() + backoff_for(code, cooldown)); - return Err(TwonlyError::Generic(format!( - "server rejected the Privacy Pass issuance with code {code}" - ))); - } - }; - if responses.len() != states.len() { - return Err(TwonlyError::Generic( - "server returned the wrong number of Privacy Pass responses".into(), - )); - } - - // Unblinding is the same kind of curve work, so it is offloaded too. - let tokens = tokio::task::spawn_blocking(move || { - responses - .into_iter() - .zip(states.iter()) - .map(|(serialized, state)| { - let response = TokenResponse::::try_from_bytes(&serialized).map_err( - |error| { - TwonlyError::Generic(format!("invalid Privacy Pass response: {error}")) - }, - )?; - response - .issue_token(state) - .map_err(|error| { - TwonlyError::Generic(format!( - "could not finalize a Privacy Pass token: {error}" - )) - })? - .tls_serialize_detached() - .map_err(|error| { - TwonlyError::Generic(format!( - "could not serialize a Privacy Pass token: {error}" - )) - }) - }) - .collect::>>>() - }) - .await - .map_err(|error| TwonlyError::Generic(format!("token finalization panicked: {error}")))??; - - let expires_at = expiry_for_todays_challenge(i64::from(parameters.max_age_seconds)); - let database = ctx.app_db.read().await.clone(); - let mut transaction = database.pool.begin().await?; - for token in tokens { - sqlx::query!( - r#"INSERT INTO privacy_pass_tokens(token, expires_at) VALUES (?, ?) - ON CONFLICT(token) DO NOTHING"#, - token, - expires_at, - ) - .execute(&mut *transaction) - .await?; - } - transaction.commit().await?; - - Ok(()) - } -} - -fn now_seconds() -> i64 { - chrono::Utc::now().timestamp() -} - -/// How long to leave the issuer alone after it refused to mint. -/// -/// The two refusals the server sends routinely are minutes apart in cost, and -/// treating them alike is what took sealed sender down for a quarter of an hour -/// over a burst the client only had to pace. -fn backoff_for(code: i32, cooldown: Duration) -> Duration { - use crate::api::proto::error::ErrorCode; - - if code == ErrorCode::TooManyRequests as i32 { - // The per-session issuance cooldown. It clears in seconds. - cooldown - } else if code == ErrorCode::PrivacyPassQuotaExhausted as i32 { - // The daily counter is keyed on the server's calendar date, so nothing - // this account does before midnight earns another token. - duration_until_next_utc_day() - } else { - UNKNOWN_REFUSAL_BACKOFF - } -} - -/// Time left in the current UTC day, which is when the issuer's daily counter -/// rolls over. Never zero, so a refusal always costs at least one wait. -fn duration_until_next_utc_day() -> Duration { - const SECONDS_PER_DAY: i64 = 24 * 60 * 60; - let now = now_seconds(); - let remaining = SECONDS_PER_DAY - now.rem_euclid(SECONDS_PER_DAY); - Duration::from_secs(remaining.max(1) as u64) -} - -/// The server derives its challenge from the current UTC day and accepts a -/// token for `max_age_seconds` counted from the start of that day, not from the -/// moment it was issued. -fn expiry_for_todays_challenge(max_age_seconds: i64) -> i64 { - const SECONDS_PER_DAY: i64 = 24 * 60 * 60; - let start_of_day = now_seconds().div_euclid(SECONDS_PER_DAY) * SECONDS_PER_DAY; - start_of_day + max_age_seconds - EXPIRY_SAFETY_MARGIN_SECONDS -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tokens_expire_with_the_day_they_were_issued_for() { - const SECONDS_PER_DAY: i64 = 24 * 60 * 60; - let expiry = expiry_for_todays_challenge(7 * SECONDS_PER_DAY); - let start_of_day = now_seconds().div_euclid(SECONDS_PER_DAY) * SECONDS_PER_DAY; - - assert_eq!( - expiry, - start_of_day + 7 * SECONDS_PER_DAY - EXPIRY_SAFETY_MARGIN_SECONDS - ); - assert!(expiry > now_seconds()); - } - - #[test] - fn the_issuance_cooldown_does_not_cost_a_quarter_of_an_hour() { - use crate::api::proto::error::ErrorCode; - - let cooldown = Duration::from_secs(5); - assert_eq!( - backoff_for(ErrorCode::TooManyRequests as i32, cooldown), - cooldown - ); - assert_eq!( - backoff_for(ErrorCode::InternalError as i32, cooldown), - UNKNOWN_REFUSAL_BACKOFF - ); - // Nothing earns a token before the daily counter rolls over, so this - // one must not come back after a mere cooldown. - assert!( - backoff_for(ErrorCode::PrivacyPassQuotaExhausted as i32, cooldown) - == duration_until_next_utc_day() - ); - } - - #[test] - fn the_daily_quota_backoff_ends_within_the_day() { - const SECONDS_PER_DAY: u64 = 24 * 60 * 60; - let remaining = duration_until_next_utc_day(); - assert!(!remaining.is_zero()); - assert!(remaining <= Duration::from_secs(SECONDS_PER_DAY)); - } -} diff --git a/rust/src/services/sealed_sender.rs b/rust/src/services/sealed_sender.rs deleted file mode 100644 index 21c03963..00000000 --- a/rust/src/services/sealed_sender.rs +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -//! Chooses between the sealed and the named transport for an outgoing message. -//! -//! A sealed envelope is uploaded anonymously, so the server never learns who -//! sent it. That only works when both sides agree: the recipient has to -//! understand the envelope, and this account has to have the feature enabled. -//! Everything else — an unknown identity key, an empty token pool, a failing -//! upload — falls back to the named transport rather than dropping the message. - -use crate::api::messages::incoming::messages::PreparedQueuedReceipt; -use crate::api::sealed_sender::SealedSenderApi; -use crate::context::Context; -use crate::error::{Result, TwonlyError}; -use crate::sealed_sender::SealedSender; -use crate::services::privacy_pass::PrivacyPassTokens; -use crate::user_config::UserConfig; -use libsignal_protocol::IdentityKeyPair; -use rand::SeedableRng; -use std::sync::Arc; - -pub(crate) struct SealedSenderService; - -impl SealedSenderService { - /// Whether messages to this contact are allowed to travel sealed. - /// - /// The contact's flag is only set once they have announced support in an - /// encrypted content of their own, so a peer that does not understand the - /// envelope never receives one. - pub(crate) async fn is_enabled_for(ctx: &Arc, contact_id: i64) -> Result { - // The contact's flag is checked first on purpose. This runs for every - // queued receipt, and reading the user configuration means a file read - // and a JSON parse under a process-wide lock, which is far more - // expensive than an indexed lookup on the primary key. - let database = ctx.app_db.read().await.clone(); - let contact_accepts = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND sealed_sender_enabled = 1)", - contact_id, - ) - .fetch_one(&database.pool) - .await? - != 0; - if !contact_accepts { - return Ok(false); - } - Ok(UserConfig::load_from(ctx)?.is_some_and(|config| config.sealed_sender_enabled)) - } - - /// Tries to deliver a prepared receipt as a sealed envelope. - /// - /// Returns `false` when the message has to go out over the named transport - /// instead. The caller may then send it normally: a receiver that ends up - /// seeing both copies discards the second one by receipt ID. - pub(crate) async fn try_send( - ctx: &Arc, - receipt: &PreparedQueuedReceipt, - ) -> Result { - if !Self::is_enabled_for(ctx, receipt.contact_id).await? { - return Ok(false); - } - - let Some(recipient_identity) = ctx.get_identity(receipt.contact_id).await? else { - tracing::info!( - contact_id = receipt.contact_id, - "no pinned identity key yet; sending this message named" - ); - return Ok(false); - }; - - let (from_user_id, sender_identity) = { - let key_manager = ctx.key_manager.lock().await; - let Some(from_user_id) = key_manager.user_id else { - return Ok(false); - }; - let Some(identity) = key_manager.signal_identity.as_ref() else { - return Ok(false); - }; - let identity = - IdentityKeyPair::try_from(identity.identity_key_pair_structure.as_slice()) - .map_err(|error| TwonlyError::Signal(error.to_string()))?; - (from_user_id, identity) - }; - - // An empty pool, or an issuer that refuses to mint, must never hold a - // message back: it just goes out named instead. - let token = match PrivacyPassTokens::take(ctx).await { - Ok(Some(token)) => token, - Ok(None) => { - tracing::info!("no Privacy Pass token available; sending this message named"); - return Ok(false); - } - Err(error) => { - tracing::warn!("could not obtain a Privacy Pass token: {error}"); - return Ok(false); - } - }; - - let envelope = { - let mut rng = rand::rngs::StdRng::from_os_rng(); - SealedSender::encrypt( - from_user_id, - receipt.contact_id, - receipt.message.clone(), - &sender_identity, - recipient_identity.public_key(), - &mut rng, - )? - }; - - match SealedSenderApi::upload(receipt.contact_id, envelope, token, receipt.wake_receiver) - .await - { - Ok(message_id) => { - tracing::info!( - receipt_id = receipt.receipt_id, - message_id, - "sent message via sealed sender" - ); - Ok(true) - } - Err(error) => { - tracing::warn!("sealed-sender upload failed, falling back to named send: {error}"); - Ok(false) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::database::app::tables::Contact; - - async fn context( - sealed_sender_enabled: bool, - ) -> anyhow::Result<(tempfile::TempDir, Arc)> { - let temp = tempfile::tempdir()?; - let data_dir = temp.path().join("data"); - std::fs::create_dir_all(data_dir.join("keyvalue"))?; - let config = UserConfig { - sealed_sender_enabled, - ..Default::default() - }; - std::fs::write( - data_dir.join("keyvalue/user.json"), - serde_json::to_vec(&config)?, - )?; - let context = Context::init_for_testing(temp.path().join("database"), data_dir).await?; - Ok((temp, context)) - } - - async fn insert_contact(ctx: &Arc, user_id: i64) -> anyhow::Result<()> { - let database = ctx.app_db.read().await.clone(); - sqlx::query!( - "INSERT INTO contacts(user_id, username, accepted) VALUES (?, ?, 1)", - user_id, - "peer", - ) - .execute(&database.pool) - .await?; - Ok(()) - } - - async fn announce(ctx: &Arc, user_id: i64, enabled: bool) -> anyhow::Result<()> { - let database = ctx.app_db.read().await.clone(); - let mut transaction = database.pool.begin().await?; - Contact::set_sealed_sender_enabled(&mut transaction, user_id, enabled).await?; - transaction.commit().await?; - Ok(()) - } - - #[tokio::test] - async fn a_contact_that_never_announced_support_keeps_the_named_transport() -> anyhow::Result<()> - { - let (_temp, ctx) = context(true).await?; - insert_contact(&ctx, 21).await?; - - assert!(!SealedSenderService::is_enabled_for(&ctx, 21).await?); - Ok(()) - } - - #[tokio::test] - async fn both_sides_have_to_enable_the_feature() -> anyhow::Result<()> { - let (_temp, ctx) = context(true).await?; - insert_contact(&ctx, 21).await?; - announce(&ctx, 21, true).await?; - - assert!(SealedSenderService::is_enabled_for(&ctx, 21).await?); - - // The contact withdrawing its announcement is enough to stop sealing. - announce(&ctx, 21, false).await?; - assert!(!SealedSenderService::is_enabled_for(&ctx, 21).await?); - Ok(()) - } - - #[tokio::test] - async fn the_local_setting_disables_sealing_for_every_contact() -> anyhow::Result<()> { - let (_temp, ctx) = context(false).await?; - insert_contact(&ctx, 21).await?; - announce(&ctx, 21, true).await?; - - assert!(!SealedSenderService::is_enabled_for(&ctx, 21).await?); - Ok(()) - } - - #[tokio::test] - async fn an_unknown_contact_is_never_sealed() -> anyhow::Result<()> { - let (_temp, ctx) = context(true).await?; - - assert!(!SealedSenderService::is_enabled_for(&ctx, 404).await?); - Ok(()) - } -} diff --git a/rust/src/user_config.rs b/rust/src/user_config.rs index ca77f423..c6904fbd 100644 --- a/rust/src/user_config.rs +++ b/rust/src/user_config.rs @@ -248,12 +248,6 @@ pub struct UserConfig { #[serde(default = "defaults::true_value")] #[frb(non_final)] pub typing_indicators: bool, - /// Announces to contacts that this account accepts sealed-sender envelopes, - /// and lets this account send them. Both sides have to have it on before - /// anything travels sealed. - #[serde(default = "defaults::true_value")] - #[frb(non_final)] - pub sealed_sender_enabled: bool, #[serde(default = "defaults::true_value")] #[frb(non_final)] pub show_restore_flame: bool, diff --git a/rust/tests/api.rs b/rust/tests/api.rs index 6791aa0d..93e7becf 100644 --- a/rust/tests/api.rs +++ b/rust/tests/api.rs @@ -10,8 +10,6 @@ mod media; mod notifications; #[path = "api/recovery.rs"] mod recovery; -#[path = "api/sealed_sender.rs"] -mod sealed_sender; #[path = "api/server_api.rs"] mod server_api; #[path = "api/session_recovery.rs"] diff --git a/rust/tests/api/sealed_sender.rs b/rust/tests/api/sealed_sender.rs deleted file mode 100644 index bb416f0d..00000000 --- a/rust/tests/api/sealed_sender.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::tester::{init_tracing, Tester}; -use rust_lib_twonly::bridge::api::ApiConnectionState; -use rust_lib_twonly::database::app::tables::Group; -use rust_lib_twonly::services::contacts::ContactService; -use rust_lib_twonly::services::messages::MessageService; - -async fn ready_tester() -> anyhow::Result { - let mut tester = Tester::new().await?; - tester.set_sealed_sender_enabled(true)?; - tester.wait_until(ApiConnectionState::Connected).await?; - tester.register_and_authenticate().await?; - tester.wait_until(ApiConnectionState::Authenticated).await?; - Ok(tester) -} - -/// Exercises the whole sealed-sender path against the running dev server: -/// Privacy Pass issuance, the capability announcement that gates it, the -/// anonymous upload, and delivery back to the recipient. -#[tokio::test] -async fn test_sealed_sender_end_to_end() -> anyhow::Result<()> { - init_tracing(); - - let tester_a = ready_tester().await?; - let tester_b = ready_tester().await?; - tracing::info!( - sender = tester_a.user_id, - recipient = tester_b.user_id, - "Testers are ready" - ); - - let group_id = Group::direct_chat_id(tester_a.user_id, tester_b.user_id); - - // Tokens are minted right after authentication, so a message never has to - // wait for a round trip before it can be sealed. - tester_a.wait_for_privacy_pass_tokens().await?; - - ContactService::new(&tester_a.context) - .request_by_username(tester_b.username.clone(), true) - .await?; - tester_b - .wait_for_contact_state(tester_a.user_id, false, true) - .await?; - ContactService::new(&tester_b.context) - .accept_request(tester_a.user_id, true) - .await?; - tester_a - .wait_for_contact_state(tester_b.user_id, true, false) - .await?; - - // The contact request and its acceptance each carry the announcement, so - // by now both sides know the other accepts sealed envelopes. - tester_a - .wait_for_contact_sealed_sender(tester_b.user_id, true) - .await?; - tester_b - .wait_for_contact_sealed_sender(tester_a.user_id, true) - .await?; - - // A sealed message arrives like any other, but the server never sees who - // sent it. - let sealed_id = MessageService::new(&tester_a.context) - .insert_and_send_text(group_id.clone(), "Sealed hello".into(), None) - .await?; - tester_b - .wait_for_text_message(&sealed_id, tester_a.user_id, "Sealed hello") - .await?; - tester_a.wait_for_message_ack_by_server(&sealed_id).await?; - assert!( - tester_a - .was_sent_sealed(&sealed_id, tester_b.user_id) - .await?, - "message was not delivered over the sealed transport" - ); - - // Turning the feature off locally has to fall back to the named transport - // without losing the message. - tester_a.set_sealed_sender_enabled(false)?; - let named_id = MessageService::new(&tester_a.context) - .insert_and_send_text(group_id.clone(), "Named hello".into(), None) - .await?; - tester_b - .wait_for_text_message(&named_id, tester_a.user_id, "Named hello") - .await?; - tester_a.wait_for_message_ack_by_server(&named_id).await?; - assert!( - !tester_a - .was_sent_sealed(&named_id, tester_b.user_id) - .await?, - "message was sealed even though the local setting is off" - ); - - // Withdrawing the announcement has to stop the peer from sealing, too. - tester_b - .wait_for_contact_sealed_sender(tester_a.user_id, false) - .await?; - tester_a.set_sealed_sender_enabled(true)?; - let back_id = MessageService::new(&tester_b.context) - .insert_and_send_text(group_id.clone(), "Back to you".into(), None) - .await?; - tester_a - .wait_for_text_message(&back_id, tester_b.user_id, "Back to you") - .await?; - assert!( - !tester_b.was_sent_sealed(&back_id, tester_a.user_id).await?, - "peer sealed a message to a contact that withdrew its announcement" - ); - - Ok(()) -} diff --git a/rust/tests/api/tester.rs b/rust/tests/api/tester.rs index 85b37eba..15f48a57 100644 --- a/rust/tests/api/tester.rs +++ b/rust/tests/api/tester.rs @@ -412,8 +412,6 @@ impl Tester { std::fs::create_dir_all(data_dir.join("keyvalue"))?; let config = rust_lib_twonly::user_config::UserConfig { - // Must be at least the server's `sealed_sender_min_app_version`, or - // the server refuses to deliver sealed-sender payloads to a tester. app_version: 119, device_id: 1, can_use_login_token_for_auth: true, @@ -421,11 +419,6 @@ impl Tester { user_discovery_threshold: 3, user_discovery_share_promotion: true, typing_indicators: true, - // Off here, on in the sealed-sender test. Minting a Privacy Pass - // batch is a run of P-384 scalar multiplications, and paying that - // in every unrelated test would slow the debug-built suite down - // for coverage the sealed-sender test already provides. - sealed_sender_enabled: false, ..Default::default() }; std::fs::write( @@ -462,81 +455,6 @@ impl Tester { }) } - pub fn set_sealed_sender_enabled(&self, enabled: bool) -> anyhow::Result<()> { - let path = self - ._temp_dir - .path() - .join("data") - .join("keyvalue") - .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.sealed_sender_enabled = enabled; - std::fs::write(&path, serde_json::to_string(&config)?)?; - Ok(()) - } - - /// Waits until the contact has announced (or withdrawn) sealed-sender - /// support. The announcement rides along with any encrypted content, so it - /// only lands once the peer has actually sent something. - pub async fn wait_for_contact_sealed_sender( - &self, - user_id: i64, - expected: bool, - ) -> anyhow::Result<()> { - for _ in 0..100 { - let database = self.context.app_db.read().await.clone(); - let enabled = sqlx::query_scalar!( - "SELECT sealed_sender_enabled FROM contacts WHERE user_id = ?", - user_id - ) - .fetch_optional(&database.pool) - .await?; - if enabled == Some(i64::from(expected)) { - return Ok(()); - } - sleep(Duration::from_millis(100)).await; - } - Err(anyhow::anyhow!( - "contact {user_id} did not reach sealed_sender_enabled = {expected}" - )) - } - - /// Whether this message's copy for `contact_id` left as a sealed envelope. - pub async fn was_sent_sealed(&self, message_id: &str, contact_id: i64) -> anyhow::Result { - let database = self.context.app_db.read().await.clone(); - Ok(sqlx::query_scalar!( - r#"SELECT EXISTS(SELECT 1 FROM message_actions - WHERE message_id = ? AND contact_id = ? AND type = 'sealedSenderAt')"#, - message_id, - contact_id, - ) - .fetch_one(&database.pool) - .await? - != 0) - } - - /// Waits for the token pool to fill. Minting is deliberately kept off the - /// send path, so it lands shortly after authentication rather than during - /// it. - /// - /// The wait is generous because blinding and unblinding a batch are P-384 - /// scalar multiplications, and this suite is built without optimizations, - /// where each one costs orders of magnitude more than in a release build. - pub async fn wait_for_privacy_pass_tokens(&self) -> anyhow::Result<()> { - for _ in 0..600 { - let database = self.context.app_db.read().await.clone(); - let count = sqlx::query_scalar!("SELECT COUNT(*) FROM privacy_pass_tokens") - .fetch_one(&database.pool) - .await?; - if count > 0 { - return Ok(()); - } - sleep(Duration::from_millis(100)).await; - } - Err(anyhow::anyhow!("the Privacy Pass token pool stayed empty")) - } - pub fn update_username(&mut self, new_username: String) -> anyhow::Result<()> { self.username = new_username.clone(); diff --git a/test/callbacks/logging_callbacks_test.dart b/test/callbacks/logging_callbacks_test.dart deleted file mode 100644 index f3c19703..00000000 --- a/test/callbacks/logging_callbacks_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:logging/logging.dart'; -import 'package:twonly/src/callbacks/logging.callbacks.dart'; - -void main() { - group('LoggingCallbacks ANSI stripping', () { - test('removes raw ANSI color escape sequences', () { - LogRecord? captured; - final sub = Logger.root.onRecord.listen((record) { - captured = record; - }); - - const rawAnsiLog = - '12:34:56 INFO dir/test.rs:42 \x1b[3mreceipt_id\x1b[0m\x1b[2m=\x1b[0m"d9891084-0f7c-4f30-958d-d8619df5a91c" Handling incoming message: FlameSync'; - - LoggingCallbacks.handleRustLog(rawAnsiLog); - - expect(captured, isNotNull); - expect(captured!.loggerName, 'dir/test.rs:42'); - expect( - captured!.message, - 'receipt_id="d9891084-0f7c-4f30-958d-d8619df5a91c" Handling incoming message: FlameSync', - ); - - sub.cancel(); - }); - - test('removes escaped caret ANSI notation', () { - LogRecord? captured; - final sub = Logger.root.onRecord.listen((record) { - captured = record; - }); - - const caretLog = - r'12:34:56 INFO dir/test.rs:42 \^[[3mreceipt_id\^[[0m\^[[2m=\^[[0m"d9891084" \^[[3mkind\^[[0m\^[[2m=\^[[0m"FlameSync" Handling incoming message'; - - LoggingCallbacks.handleRustLog(caretLog); - - expect(captured, isNotNull); - expect( - captured!.message, - 'receipt_id="d9891084" kind="FlameSync" Handling incoming message', - ); - - sub.cancel(); - }); - }); -} diff --git a/test/mocks/user_config.dart b/test/mocks/user_config.dart index deeb4063..c578f42a 100644 --- a/test/mocks/user_config.dart +++ b/test/mocks/user_config.dart @@ -34,7 +34,6 @@ UserConfig testUserConfig({ screenLockEnabled: false, isCloudBackupEnabled: false, isUserDiscoveryEnabled: false, - sealedSenderEnabled: true, requiredSendImages: 4, userDiscoveryThreshold: 3, userDiscoveryRequiresManualApproval: false,