diff --git a/lib/core/bridge.dart b/lib/core/bridge.dart index 221c3e17..acd01aeb 100644 --- a/lib/core/bridge.dart +++ b/lib/core/bridge.dart @@ -7,7 +7,7 @@ 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`: `get_twonly_flutter` -// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AnnouncedUser`, `OtherPromotion`, `TwonlyFlutter` +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AnnouncedUser`, `OtherPromotion` Future initializeTwonlyFlutter({required InitConfig config}) => RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config); diff --git a/lib/core/bridge/api.dart b/lib/core/bridge/api.dart index 8d15fe59..0e7909e5 100644 --- a/lib/core/bridge/api.dart +++ b/lib/core/bridge/api.dart @@ -56,27 +56,6 @@ enum ApiEventKind { loginTokenMigrated, } -class PreparedOutgoingMessage { - final Uint8List message; - final Uint8List? pushData; - - const PreparedOutgoingMessage({ - required this.message, - this.pushData, - }); - - @override - int get hashCode => message.hashCode ^ pushData.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is PreparedOutgoingMessage && - runtimeType == other.runtimeType && - message == other.message && - pushData == other.pushData; -} - /// Flutter-facing facade for the Rust-owned API runtime. class RustApi { const RustApi(); @@ -255,11 +234,10 @@ class RustApi { .api .crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat(); - static Future prepareQueuedMessage({ - required String receiptId, - }) => RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage( - receiptId: receiptId, - ); + static Future prepareQueuedMessage({required String receiptId}) => + RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage( + receiptId: receiptId, + ); static Future register({ required String username, @@ -348,7 +326,7 @@ class RustApi { contactId: contactId, ); - static Future sendEncryptedContent({ + static Future sendEncryptedContent({ required PlatformInt64 contactId, required List content, String? messageId, @@ -384,11 +362,9 @@ class RustApi { static Future sendTextMessage({ required PlatformInt64 userId, required List body, - Uint8List? pushData, }) => RustLib.instance.api.crateBridgeApiRustApiSendTextMessage( userId: userId, body: body, - pushData: pushData, ); static Future sendTyping({ diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 090a9ec4..53f742e3 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -292,7 +292,7 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat(); - Future crateBridgeApiRustApiPrepareQueuedMessage({ + Future crateBridgeApiRustApiPrepareQueuedMessage({ required String receiptId, }); @@ -352,7 +352,7 @@ abstract class RustLibApi extends BaseApi { required PlatformInt64 contactId, }); - Future crateBridgeApiRustApiSendEncryptedContent({ + Future crateBridgeApiRustApiSendEncryptedContent({ required PlatformInt64 contactId, required List content, String? messageId, @@ -375,7 +375,6 @@ abstract class RustLibApi extends BaseApi { Future crateBridgeApiRustApiSendTextMessage({ required PlatformInt64 userId, required List body, - Uint8List? pushData, }); Future crateBridgeApiRustApiSendTyping({ @@ -2329,7 +2328,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiPrepareQueuedMessage({ + Future crateBridgeApiRustApiPrepareQueuedMessage({ required String receiptId, }) { return handler.executeNormal( @@ -2345,8 +2344,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); }, codec: SseCodec( - decodeSuccessData: - sse_decode_opt_box_autoadd_prepared_outgoing_message, + decodeSuccessData: sse_decode_opt_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiPrepareQueuedMessageConstMeta, @@ -2812,7 +2810,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateBridgeApiRustApiSendEncryptedContent({ + Future crateBridgeApiRustApiSendEncryptedContent({ required PlatformInt64 contactId, required List content, String? messageId, @@ -2838,8 +2836,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); }, codec: SseCodec( - decodeSuccessData: - sse_decode_opt_box_autoadd_prepared_outgoing_message, + decodeSuccessData: sse_decode_opt_list_prim_u_8_strict, decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiSendEncryptedContentConstMeta, @@ -2951,7 +2948,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { Future crateBridgeApiRustApiSendTextMessage({ required PlatformInt64 userId, required List body, - Uint8List? pushData, }) { return handler.executeNormal( NormalTask( @@ -2959,7 +2955,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_i_64(userId, serializer); sse_encode_list_prim_u_8_loose(body, serializer); - sse_encode_opt_list_prim_u_8_strict(pushData, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, @@ -2972,7 +2967,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateBridgeApiRustApiSendTextMessageConstMeta, - argValues: [userId, body, pushData], + argValues: [userId, body], apiImpl: this, ), ); @@ -2981,7 +2976,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateBridgeApiRustApiSendTextMessageConstMeta => const TaskConstMeta( debugName: "rust_api_send_text_message", - argNames: ["userId", "body", "pushData"], + argNames: ["userId", "body"], ); @override @@ -5302,14 +5297,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_passwordless_recovery_config(raw); } - @protected - PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dco_decode_prepared_outgoing_message(raw); - } - @protected TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5618,16 +5605,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { : dco_decode_box_autoadd_passwordless_recovery_config(raw); } - @protected - PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message( - dynamic raw, - ) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null - ? null - : dco_decode_box_autoadd_prepared_outgoing_message(raw); - } - @protected TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -5692,18 +5669,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - @protected - PreparedOutgoingMessage dco_decode_prepared_outgoing_message(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 2) - throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); - return PreparedOutgoingMessage( - message: dco_decode_list_prim_u_8_strict(arr[0]), - pushData: dco_decode_opt_list_prim_u_8_strict(arr[1]), - ); - } - @protected (PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict( dynamic raw, @@ -6197,14 +6162,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_passwordless_recovery_config(deserializer)); } - @protected - PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - return (sse_decode_prepared_outgoing_message(deserializer)); - } - @protected TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup( SseDeserializer deserializer, @@ -6632,19 +6589,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_box_autoadd_prepared_outgoing_message(deserializer)); - } else { - return null; - } - } - @protected TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup( SseDeserializer deserializer, @@ -6754,19 +6698,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - @protected - PreparedOutgoingMessage sse_decode_prepared_outgoing_message( - SseDeserializer deserializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_message = sse_decode_list_prim_u_8_strict(deserializer); - var var_pushData = sse_decode_opt_list_prim_u_8_strict(deserializer); - return PreparedOutgoingMessage( - message: var_message, - pushData: var_pushData, - ); - } - @protected (PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict( SseDeserializer deserializer, @@ -7442,15 +7373,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_passwordless_recovery_config(self, serializer); } - @protected - void sse_encode_box_autoadd_prepared_outgoing_message( - PreparedOutgoingMessage self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_prepared_outgoing_message(self, serializer); - } - @protected void sse_encode_box_autoadd_twonly_safe_backup( TwonlySafeBackup self, @@ -7834,19 +7756,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - void sse_encode_opt_box_autoadd_prepared_outgoing_message( - PreparedOutgoingMessage? self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_box_autoadd_prepared_outgoing_message(self, serializer); - } - } - @protected void sse_encode_opt_box_autoadd_twonly_safe_backup( TwonlySafeBackup? self, @@ -7940,16 +7849,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_strict(self.kyberPreKeySignature, serializer); } - @protected - void sse_encode_prepared_outgoing_message( - PreparedOutgoingMessage self, - SseSerializer serializer, - ) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_list_prim_u_8_strict(self.message, serializer); - sse_encode_opt_list_prim_u_8_strict(self.pushData, serializer); - } - @protected void sse_encode_record_i_64_list_prim_u_8_strict( (PlatformInt64, Uint8List) self, diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart index ff3602be..cd38253a 100644 --- a/lib/core/frb_generated.io.dart +++ b/lib/core/frb_generated.io.dart @@ -150,11 +150,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { PasswordlessRecoveryConfig dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw); - @protected - PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message( - dynamic raw, - ); - @protected TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw); @@ -283,11 +278,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { PasswordlessRecoveryConfig? dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw); - @protected - PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message( - dynamic raw, - ); - @protected TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw); @@ -311,9 +301,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw); - @protected - PreparedOutgoingMessage dco_decode_prepared_outgoing_message(dynamic raw); - @protected (PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict( dynamic raw, @@ -488,11 +475,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); - @protected - PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message( - SseDeserializer deserializer, - ); - @protected TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup( SseDeserializer deserializer, @@ -641,11 +623,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); - @protected - PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message( - SseDeserializer deserializer, - ); - @protected TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup( SseDeserializer deserializer, @@ -673,11 +650,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer); - @protected - PreparedOutgoingMessage sse_decode_prepared_outgoing_message( - SseDeserializer deserializer, - ); - @protected (PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict( SseDeserializer deserializer, @@ -938,12 +910,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_box_autoadd_prepared_outgoing_message( - PreparedOutgoingMessage self, - SseSerializer serializer, - ); - @protected void sse_encode_box_autoadd_twonly_safe_backup( TwonlySafeBackup self, @@ -1124,12 +1090,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_opt_box_autoadd_prepared_outgoing_message( - PreparedOutgoingMessage? self, - SseSerializer serializer, - ); - @protected void sse_encode_opt_box_autoadd_twonly_safe_backup( TwonlySafeBackup? self, @@ -1166,12 +1126,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_prepared_outgoing_message( - PreparedOutgoingMessage self, - SseSerializer serializer, - ); - @protected void sse_encode_record_i_64_list_prim_u_8_strict( (PlatformInt64, Uint8List) self, diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart index 112e04a6..462b9958 100644 --- a/lib/core/frb_generated.web.dart +++ b/lib/core/frb_generated.web.dart @@ -152,11 +152,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { PasswordlessRecoveryConfig dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw); - @protected - PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message( - dynamic raw, - ); - @protected TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw); @@ -285,11 +280,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { PasswordlessRecoveryConfig? dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw); - @protected - PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message( - dynamic raw, - ); - @protected TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw); @@ -313,9 +303,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw); - @protected - PreparedOutgoingMessage dco_decode_prepared_outgoing_message(dynamic raw); - @protected (PlatformInt64, Uint8List) dco_decode_record_i_64_list_prim_u_8_strict( dynamic raw, @@ -490,11 +477,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); - @protected - PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message( - SseDeserializer deserializer, - ); - @protected TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup( SseDeserializer deserializer, @@ -643,11 +625,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); - @protected - PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message( - SseDeserializer deserializer, - ); - @protected TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup( SseDeserializer deserializer, @@ -675,11 +652,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer); - @protected - PreparedOutgoingMessage sse_decode_prepared_outgoing_message( - SseDeserializer deserializer, - ); - @protected (PlatformInt64, Uint8List) sse_decode_record_i_64_list_prim_u_8_strict( SseDeserializer deserializer, @@ -940,12 +912,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_box_autoadd_prepared_outgoing_message( - PreparedOutgoingMessage self, - SseSerializer serializer, - ); - @protected void sse_encode_box_autoadd_twonly_safe_backup( TwonlySafeBackup self, @@ -1126,12 +1092,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_opt_box_autoadd_prepared_outgoing_message( - PreparedOutgoingMessage? self, - SseSerializer serializer, - ); - @protected void sse_encode_opt_box_autoadd_twonly_safe_backup( TwonlySafeBackup? self, @@ -1168,12 +1128,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); - @protected - void sse_encode_prepared_outgoing_message( - PreparedOutgoingMessage self, - SseSerializer serializer, - ); - @protected void sse_encode_record_i_64_list_prim_u_8_strict( (PlatformInt64, Uint8List) self, diff --git a/lib/src/services/api/mediafiles/upload.api.dart b/lib/src/services/api/mediafiles/upload.api.dart index 04b71793..8e786e79 100644 --- a/lib/src/services/api/mediafiles/upload.api.dart +++ b/lib/src/services/api/mediafiles/upload.api.dart @@ -691,11 +691,13 @@ Future _createUploadRequest(MediaFileService media) async { ); } - final cipherText = await sendCipherText( - groupMember.contactId, - notEncryptedContent, + final cipherText = await RustApi.sendEncryptedContent( + contactId: groupMember.contactId, + content: notEncryptedContent.writeToBuffer(), messageId: message.messageId, + onlySendIfNoReceiptsAreOpen: false, onlyReturnEncryptedData: true, + blocking: true, ); if (cipherText == null) { @@ -706,13 +708,9 @@ Future _createUploadRequest(MediaFileService media) async { } final messageOnSuccess = TextMessage() - ..body = cipherText.$1 + ..body = cipherText ..userId = Int64(groupMember.contactId); - if (cipherText.$2 != null) { - messageOnSuccess.pushData = cipherText.$2!; - } - messagesOnSuccess.add(messageOnSuccess); downloadTokens.add(downloadToken); } diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index 8e5b688a..481524e0 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -1,13 +1,9 @@ import 'dart:typed_data'; -import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' - as frb; import 'package:twonly/core/bridge/api.dart' as rust_api; import 'package:twonly/src/database/twonly.db.dart' show Receipt; -import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' - as pb; -Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ +Future tryToSendCompleteMessage({ String? receiptId, Receipt? receipt, bool onlyReturnEncryptedData = false, @@ -19,86 +15,5 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ await rust_api.RustApi.sendQueuedMessage(receiptId: id); return null; } - final prepared = await rust_api.RustApi.prepareQueuedMessage(receiptId: id); - return prepared == null ? null : (prepared.message, prepared.pushData); + return rust_api.RustApi.prepareQueuedMessage(receiptId: id); } - -Future insertAndSendTextMessage( - String groupId, - String textMessage, - String? quotesMessageId, -) async { - await rust_api.RustApi.insertAndSendText( - groupId: groupId, - text: textMessage, - quoteMessageId: quotesMessageId, - ); -} - -Future insertAndSendContactShareMessage( - String groupId, - List contactsToShare, -) async { - await rust_api.RustApi.insertAndSendContactShare( - groupId: groupId, - contactIds: frb.Int64List.fromList(contactsToShare), - ); -} - -Future insertAndSendAskAboutUserMessage( - int contactId, - int askAboutUserId, -) async { - await rust_api.RustApi.insertAndSendAskAboutUser( - contactId: contactId, - askAboutUserId: askAboutUserId, - ); -} - -Future sendCipherTextToGroup( - String groupId, - pb.EncryptedContent encryptedContent, { - String? messageId, - bool onlySendIfNoReceiptsAreOpen = false, -}) async { - await rust_api.RustApi.sendEncryptedContentToGroup( - groupId: groupId, - content: encryptedContent.writeToBuffer(), - messageId: messageId, - onlySendIfNoReceiptsAreOpen: onlySendIfNoReceiptsAreOpen, - ); -} - -Future<(Uint8List, Uint8List?)?> sendCipherText( - int contactId, - pb.EncryptedContent encryptedContent, { - bool onlyReturnEncryptedData = false, - bool blocking = true, - String? messageId, - bool onlySendIfNoReceiptsAreOpen = false, -}) async { - final prepared = await rust_api.RustApi.sendEncryptedContent( - contactId: contactId, - content: encryptedContent.writeToBuffer(), - messageId: messageId, - onlySendIfNoReceiptsAreOpen: onlySendIfNoReceiptsAreOpen, - onlyReturnEncryptedData: onlyReturnEncryptedData, - blocking: blocking, - ); - if (!onlyReturnEncryptedData || prepared == null) return null; - return (prepared.message, prepared.pushData); -} - -Future sendTypingIndication(String groupId, bool isTyping) => - rust_api.RustApi.sendTyping(groupId: groupId, isTyping: isTyping); - -Future notifyContactAboutOpeningMessage( - int contactId, - List messageOtherIds, -) => rust_api.RustApi.notifyMessagesOpened( - contactId: contactId, - messageIds: messageOtherIds, -); - -Future sendContactMyProfileData(int contactId) => - rust_api.RustApi.sendContactProfile(contactId: contactId); diff --git a/lib/src/services/api/utils.api.dart b/lib/src/services/api/utils.api.dart index d84cf8c2..2edfbe99 100644 --- a/lib/src/services/api/utils.api.dart +++ b/lib/src/services/api/utils.api.dart @@ -10,7 +10,6 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart as server; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' hide Message; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -62,14 +61,17 @@ Future handleMediaError(MediaFile media) async { if (messages.length != 1) return; final message = messages.first; if (message.senderId == null) return; - await sendCipherText( - message.senderId!, - EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: message.senderId!, + content: EncryptedContent( mediaUpdate: EncryptedContent_MediaUpdate( type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR, targetMessageId: message.messageId, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); } @@ -81,13 +83,16 @@ Future importSignalContactAndCreateRequest( } // 2. Then send user request - await sendCipherText( - userdata.userId.toInt(), - EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: userdata.userId.toInt(), + content: EncryptedContent( contactRequest: EncryptedContent_ContactRequest( type: EncryptedContent_ContactRequest_Type.REQUEST, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); return true; diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index d7613e38..5ab34537 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -5,7 +5,6 @@ import 'package:fixnum/fixnum.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -39,9 +38,9 @@ Future syncFlameCounters({String? forceForGroup}) async { continue; } - await sendCipherTextToGroup( - group.groupId, - EncryptedContent( + await RustApi.sendEncryptedContentToGroup( + groupId: group.groupId, + content: EncryptedContent( flameSync: EncryptedContent_FlameSync( flameCounter: Int64(flameResult.counter), lastFlameCounterChange: Int64( @@ -50,7 +49,8 @@ Future syncFlameCounters({String? forceForGroup}) async { bestFriend: group.groupId == bestFriend.groupId, forceUpdate: group.groupId == forceForGroup, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, ); await twonlyDB.groupsDao.updateGroup( diff --git a/lib/src/services/key_verification.service.dart b/lib/src/services/key_verification.service.dart index df656698..6dbcc415 100644 --- a/lib/src/services/key_verification.service.dart +++ b/lib/src/services/key_verification.service.dart @@ -10,7 +10,6 @@ import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; import 'package:twonly/src/providers/routing.provider.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; @@ -56,13 +55,16 @@ class KeyVerificationService { false, ); - await sendCipherText( - contactId, - pb.EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: contactId, + content: pb.EncryptedContent( keyVerificationProof: pb.EncryptedContent_KeyVerificationProof( calculatedMac: calculatedMac, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); } diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index 242eb503..a25476b9 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -22,7 +22,6 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart'; import 'package:twonly/src/providers/routing.provider.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; @@ -152,13 +151,16 @@ class PasswordlessRecoveryService { for (final contact in oldTrustedFriends) { try { - await sendCipherText( - contact.userId, - pb.EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: contact.userId, + content: pb.EncryptedContent( passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery( delete: true, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); } catch (e) { Log.error( @@ -482,13 +484,16 @@ class PasswordlessRecoveryService { // It's PIN, we can't migrate it because we don't have the PIN. We delete it so the user has to do it again. for (final contact in oldTrustedFriends) { try { - await sendCipherText( - contact.userId, - pb.EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: contact.userId, + content: pb.EncryptedContent( passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery( delete: true, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); } catch (e) { Log.error( diff --git a/lib/src/services/signal/encryption.signal.dart b/lib/src/services/signal/encryption.signal.dart index 4810048f..d5cdf7f2 100644 --- a/lib/src/services/signal/encryption.signal.dart +++ b/lib/src/services/signal/encryption.signal.dart @@ -3,10 +3,9 @@ import 'dart:typed_data'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; // ignore: implementation_imports import 'package:libsignal_protocol_dart/src/invalid_message_exception.dart'; -import 'package:twonly/core/bridge/wrapper/signal.dart'; +import 'package:twonly/locator.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/signal/protocol_state.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; @@ -23,106 +22,29 @@ Future signalEncryptMessage( Uint8List plaintextContent, ) async { return lockingSignalProtocol.protect(() async { - return _signalEncryptMessageV1(target, plaintextContent); - }); -} - -Future signalEncryptMessageV2( - int target, - Uint8List plaintextContent, -) async { - try { - final res = await RustSignal.encrypt( - name: target.toString(), - deviceId: 1, - plaintext: plaintextContent, - ); - return SignalEncryptResult(res, pb.Message_Type.CIPHERTEXT_V2); - } catch (e) { - Log.error('Could not encrypt message (V2) for target $target: $e'); - return null; - } -} - -Future _signalEncryptMessageV1( - int target, - Uint8List plaintextContent, -) async { - try { - final signalStore = (await getSignalStore())!; - final address = getSignalAddress(target); - final session = SessionCipher.fromStore(signalStore, address); - final cipherText = await session.encrypt(plaintextContent); - - pb.Message_Type type; - switch (cipherText.getType()) { - case CiphertextMessage.prekeyType: - type = pb.Message_Type.PREKEY_BUNDLE; - case CiphertextMessage.whisperType: - type = pb.Message_Type.CIPHERTEXT; - default: - Log.error('Invalid ciphertext type: ${cipherText.getType()}.'); - return null; - } - - return SignalEncryptResult(cipherText.serialize(), type); - } catch (e) { - Log.error('Could not encrypt message (V1) for target $target: $e'); - return null; - } -} - -Future<(pb.EncryptedContent?, pb.PlaintextContent_DecryptionErrorMessage_Type?)> -signalDecryptMessageV2( - int fromUserId, - Uint8List encryptedContentRaw, { - Set? brokenSessionsInCurrentBatch, -}) async { - Log.info('Acquiring lockingSignalProtocol for $fromUserId (V2)'); - final ( - decryptedContent, - errorType, - needsResync, - ) = await lockingSignalProtocol.protect(() async { - Log.info('Lock acquired for $fromUserId (V2)'); try { - final plaintext = await RustSignal.decrypt( - name: fromUserId.toString(), - deviceId: 1, - ciphertext: encryptedContentRaw, - ); - recordResyncAttempt(fromUserId, success: true); - return (pb.EncryptedContent.fromBuffer(plaintext), null, false); + final signalStore = (await getSignalStore())!; + final address = getSignalAddress(target); + final session = SessionCipher.fromStore(signalStore, address); + final cipherText = await session.encrypt(plaintextContent); + + pb.Message_Type type; + switch (cipherText.getType()) { + case CiphertextMessage.prekeyType: + type = pb.Message_Type.PREKEY_BUNDLE; + case CiphertextMessage.whisperType: + type = pb.Message_Type.CIPHERTEXT; + default: + Log.error('Invalid ciphertext type: ${cipherText.getType()}.'); + return null; + } + + return SignalEncryptResult(cipherText.serialize(), type); } catch (e) { - Log.error('Could not decrypt message (V2) from $fromUserId: $e'); - return ( - null, - pb.PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN, - true, // Needs resync on failure - ); + Log.error('Could not encrypt message for target $target: $e'); + return null; } }); - - Log.info('Released lockingSignalProtocol for $fromUserId (V2)'); - - if (needsResync) { - brokenSessionsInCurrentBatch?.add(fromUserId); - if (shouldAttemptResync(fromUserId)) { - if (await handleSessionResync(fromUserId)) { - recordResyncAttempt(fromUserId, success: false); - await sendCipherText( - fromUserId, - pb.EncryptedContent( - errorMessages: pb.EncryptedContent_ErrorMessages( - type: pb.EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC, - ), - ), - ); - } - } - } - - return (decryptedContent, errorType); } Future<(pb.EncryptedContent?, pb.PlaintextContent_DecryptionErrorMessage_Type?)> @@ -218,13 +140,16 @@ signalDecryptMessageV1( // This message contains a new PreKeyBundle establishing a new signal // session - await sendCipherText( - fromUserId, - pb.EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: fromUserId, + content: pb.EncryptedContent( errorMessages: pb.EncryptedContent_ErrorMessages( type: pb.EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); } } diff --git a/lib/src/visual/components/select_chat_deletion_time.comp.dart b/lib/src/visual/components/select_chat_deletion_time.comp.dart index 7985dcbc..479a5e35 100644 --- a/lib/src/visual/components/select_chat_deletion_time.comp.dart +++ b/lib/src/visual/components/select_chat_deletion_time.comp.dart @@ -8,7 +8,6 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; @@ -96,14 +95,15 @@ class _SelectChatDeletionTimeListTitleState ), ), ); - await sendCipherTextToGroup( - group!.groupId, - EncryptedContent( + await RustApi.sendEncryptedContentToGroup( + groupId: group!.groupId, + content: EncryptedContent( groupUpdate: EncryptedContent_GroupUpdate( groupActionType: GroupActionType.changeDisplayMaxTime.name, newDeleteMessagesAfterMilliseconds: Int64(selected), ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, ); } else { if (!await updateChatDeletionTime(group!, selected)) { diff --git a/lib/src/visual/views/chats/chat_messages.view.dart b/lib/src/visual/views/chats/chat_messages.view.dart index cf1ca96d..01956147 100644 --- a/lib/src/visual/views/chats/chat_messages.view.dart +++ b/lib/src/visual/views/chats/chat_messages.view.dart @@ -14,7 +14,6 @@ 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/model/protobuf/client/generated/data.pb.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -179,12 +178,12 @@ class _ChatMessagesViewState extends State } if (userService.currentUser.typingIndicators) { - unawaited(sendTypingIndication(widget.groupId, false)); + unawaited(RustApi.sendTyping(groupId: widget.groupId, isTyping: false)); _nextTypingIndicator = Timer.periodic(const Duration(seconds: 2), ( _, ) async { if (_isViewActive()) { - await sendTypingIndication(widget.groupId, false); + await RustApi.sendTyping(groupId: widget.groupId, isTyping: false); } }); } @@ -437,9 +436,9 @@ class _ChatMessagesViewState extends State openedMessages[contactId]!, ); unawaited( - notifyContactAboutOpeningMessage( - contactId, - openedMessages[contactId]!, + RustApi.notifyMessagesOpened( + contactId: contactId, + messageIds: openedMessages[contactId]!, ), ); } diff --git a/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart b/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart index 26bb3cb7..5be6e09b 100644 --- a/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart +++ b/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/all_reactions.bottom_sheet.dart @@ -6,7 +6,6 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/animate_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -56,15 +55,16 @@ class _AllReactionsViewState extends State { emoji, true, ); - await sendCipherTextToGroup( - widget.message.groupId, - pb.EncryptedContent( + await RustApi.sendEncryptedContentToGroup( + groupId: widget.message.groupId, + content: pb.EncryptedContent( reaction: pb.EncryptedContent_Reaction( targetMessageId: widget.message.messageId, emoji: emoji, remove: true, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, ); } diff --git a/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart b/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart index f7258ae4..02b719ff 100644 --- a/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart +++ b/lib/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart @@ -1,9 +1,10 @@ import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' + show Int64List; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/shared/select_contacts.view.dart'; @@ -40,9 +41,9 @@ class _ShareAdditionalViewState extends State { ) as List?; if (selectedContacts != null && selectedContacts.isNotEmpty) { - await insertAndSendContactShareMessage( - widget.group.groupId, - selectedContacts, + await RustApi.insertAndSendContactShare( + groupId: widget.group.groupId, + contactIds: Int64List.fromList(selectedContacts), ); if (widget.group.isDirectChat) { final members = await twonlyDB.groupsDao.getGroupContact( diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart index 3fe149b4..671708c1 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_audio_entry.dart @@ -1,9 +1,9 @@ import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/visual/elements/better_text.element.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/entries/common.dart'; @@ -217,9 +217,9 @@ class _InChatAudioPlayerState extends State { _playerController.startPlayer(); if (widget.message.senderId != null && widget.message.openedAt == null) { - notifyContactAboutOpeningMessage( - widget.message.senderId!, - [widget.message.messageId], + RustApi.notifyMessagesOpened( + contactId: widget.message.senderId!, + messageIds: [widget.message.messageId], ); } } diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_media_entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_media_entry.dart index ffd3cebb..55653e82 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_media_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_media_entry.dart @@ -12,7 +12,6 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' hide Message; import 'package:twonly/src/services/api/mediafiles/download.api.dart' as received; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/better_text.element.dart'; @@ -78,14 +77,17 @@ class _ChatMediaEntryState extends State { } if (widget.mediaService.canBeOpenedAgain && widget.message.senderId != null) { - await sendCipherText( - widget.message.senderId!, - EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: widget.message.senderId!, + content: EncryptedContent( mediaUpdate: EncryptedContent_MediaUpdate( type: EncryptedContent_MediaUpdate_Type.REOPENED, targetMessageId: widget.message.messageId, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); await twonlyDB.messagesDao.updateMessageId( widget.message.messageId, diff --git a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart index 085a779e..c1ae5eac 100644 --- a/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart +++ b/lib/src/visual/views/chats/chat_messages_components/message_context_menu.dart @@ -12,7 +12,6 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' as pb; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/emoji_picker.bottom.dart'; @@ -53,14 +52,17 @@ class MessageContextMenu extends StatelessWidget { if (message.senderId != null) { // notify the sender - await sendCipherText( - message.senderId!, - pb.EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: message.senderId!, + content: pb.EncryptedContent( mediaUpdate: pb.EncryptedContent_MediaUpdate( type: pb.EncryptedContent_MediaUpdate_Type.REOPENED, targetMessageId: message.messageId, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); await twonlyDB.messagesDao.updateMessageId( message.messageId, @@ -125,15 +127,16 @@ class MessageContextMenu extends StatelessWidget { false, ); - await sendCipherTextToGroup( - message.groupId, - pb.EncryptedContent( + await RustApi.sendEncryptedContentToGroup( + groupId: message.groupId, + content: pb.EncryptedContent( reaction: pb.EncryptedContent_Reaction( targetMessageId: message.messageId, emoji: layer.text, remove: false, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, ); }, icon: FontAwesomeIcons.faceLaugh, @@ -186,14 +189,15 @@ class MessageContextMenu extends StatelessWidget { message.messageId, clock.now(), ); - await sendCipherTextToGroup( - message.groupId, - pb.EncryptedContent( + await RustApi.sendEncryptedContentToGroup( + groupId: message.groupId, + content: pb.EncryptedContent( messageUpdate: pb.EncryptedContent_MessageUpdate( type: pb.EncryptedContent_MessageUpdate_Type.DELETE, senderMessageId: message.messageId, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, ); } else if (action == 'delete_for_me') { await twonlyDB.messagesDao.deleteMessagesById( @@ -403,9 +407,9 @@ Future editTextMessage(BuildContext context, Message message) async { newText!, timestamp, ); - await sendCipherTextToGroup( - message.groupId, - pb.EncryptedContent( + await RustApi.sendEncryptedContentToGroup( + groupId: message.groupId, + content: pb.EncryptedContent( messageUpdate: pb.EncryptedContent_MessageUpdate( type: pb @@ -417,7 +421,8 @@ Future editTextMessage(BuildContext context, Message message) async { timestamp.millisecondsSinceEpoch, ), ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, ); } if (!context.mounted) return; 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 b7b12373..b532bf56 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 @@ -14,7 +14,6 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/visual/views/chats/chat_messages_components/bottom_sheets/share_additional.bottom_sheet.dart'; @@ -64,10 +63,10 @@ class _MessageInputState extends State { Future _sendMessage() async { if (_textFieldController.text == '') return; - await insertAndSendTextMessage( - widget.group.groupId, - _textFieldController.text, - widget.quotesMessage?.messageId, + await RustApi.insertAndSendText( + groupId: widget.group.groupId, + text: _textFieldController.text, + quoteMessageId: widget.quotesMessage?.messageId, ); _textFieldController.clear(); @@ -93,7 +92,7 @@ class _MessageInputState extends State { _lastTextChangeTime != null && DateTime.now().difference(_lastTextChangeTime!) <= const Duration(seconds: 6)) { - await sendTypingIndication(widget.group.groupId, true); + await RustApi.sendTyping(groupId: widget.group.groupId, isTyping: true); } }); } diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index adea9d7b..6c63694a 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -17,7 +17,6 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; import 'package:twonly/src/services/api/mediafiles/download.api.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; @@ -417,9 +416,9 @@ class _MediaViewerViewState extends State { markAsOpenMessageIDs = messageIds; } - await notifyContactAboutOpeningMessage( - currentMessage!.senderId!, - markAsOpenMessageIDs, + await RustApi.notifyMessagesOpened( + contactId: currentMessage!.senderId!, + messageIds: markAsOpenMessageIDs, ); } @@ -531,14 +530,15 @@ class _MediaViewerViewState extends State { mediaStored: Value(true), ), ); - await sendCipherTextToGroup( - widget.group.groupId, - pb.EncryptedContent( + await RustApi.sendEncryptedContentToGroup( + groupId: widget.group.groupId, + content: pb.EncryptedContent( mediaUpdate: pb.EncryptedContent_MediaUpdate( type: pb.EncryptedContent_MediaUpdate_Type.STORED, targetMessageId: msg.messageId, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, ); setState(() { imageSaved = true; @@ -620,10 +620,10 @@ class _MediaViewerViewState extends State { void _sendTextMessage() { if (textMessageController.text.isNotEmpty) { unawaited( - insertAndSendTextMessage( - widget.group.groupId, - textMessageController.text, - currentMessage!.messageId, + RustApi.insertAndSendText( + groupId: widget.group.groupId, + text: textMessageController.text, + quoteMessageId: currentMessage!.messageId, ), ); textMessageController.clear(); diff --git a/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart b/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart index d76692f0..3c1dd70b 100644 --- a/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart +++ b/lib/src/visual/views/chats/media_viewer_components/emoji_reactions_row.comp.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/visual/components/animate_icon.comp.dart'; import 'package:twonly/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart'; @@ -29,15 +28,16 @@ Future sendReaction( emoji, false, ); - await sendCipherTextToGroup( - groupId, - EncryptedContent( + await RustApi.sendEncryptedContentToGroup( + groupId: groupId, + content: EncryptedContent( reaction: EncryptedContent_Reaction( targetMessageId: messageId, emoji: emoji, remove: false, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, ); } diff --git a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart index ff3f944b..b7d706e4 100644 --- a/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart +++ b/lib/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart @@ -5,7 +5,6 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -150,7 +149,10 @@ class FriendSuggestionsComp extends StatelessWidget { if (result == true && selectedFriends.isNotEmpty) { for (final contactId in selectedFriends) { - await insertAndSendAskAboutUserMessage(contactId, user.announcedUserId); + await RustApi.insertAndSendAskAboutUser( + contactId: contactId, + askAboutUserId: user.announcedUserId, + ); } await twonlyDB.userDiscoveryDao.updateAnnouncedUser( diff --git a/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart b/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart index cc6fdfed..be2778e0 100644 --- a/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart +++ b/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart @@ -5,7 +5,6 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -111,15 +110,18 @@ class OpenRequestsListComp extends StatelessWidget { groupName: Value(getContactDisplayName(contact)), ), ); - await sendCipherText( - contact.userId, - EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: contact.userId, + content: EncryptedContent( contactRequest: EncryptedContent_ContactRequest( type: EncryptedContent_ContactRequest_Type.ACCEPT, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); - await sendContactMyProfileData(contact.userId); + await RustApi.sendContactProfile(contactId: contact.userId); }, ), ), @@ -131,13 +133,16 @@ class OpenRequestsListComp extends StatelessWidget { constraints: const BoxConstraints(), icon: const Icon(Icons.close, size: 18), onPressed: () async { - await sendCipherText( - contact.userId, - EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: contact.userId, + content: EncryptedContent( contactRequest: EncryptedContent_ContactRequest( type: EncryptedContent_ContactRequest_Type.REJECT, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); await twonlyDB.contactsDao.updateContact( contact.userId, diff --git a/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart b/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart index 1768a046..b83e85e1 100644 --- a/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart +++ b/lib/src/visual/views/contact/contact_components/restore_flame.comp.dart @@ -12,7 +12,6 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/log.dart'; @@ -100,10 +99,11 @@ class _RestoreFlameCompState extends State { ); await syncFlameCounters(forceForGroup: _groupId); - await sendCipherTextToGroup( - _groupId, - encryptedContent, + await RustApi.sendEncryptedContentToGroup( + groupId: _groupId, + content: encryptedContent.writeToBuffer(), messageId: message.messageId, + onlySendIfNoReceiptsAreOpen: false, ); } diff --git a/lib/src/visual/views/groups/group_member.context.dart b/lib/src/visual/views/groups/group_member.context.dart index 33d675df..f5bd6a69 100644 --- a/lib/src/visual/views/groups/group_member.context.dart +++ b/lib/src/visual/views/groups/group_member.context.dart @@ -8,7 +8,6 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; @@ -99,13 +98,16 @@ class GroupMemberContextMenu extends StatelessWidget { deletedByUser: Value(false), ), ); - await sendCipherText( - member.contactId, - EncryptedContent( + await RustApi.sendEncryptedContent( + contactId: member.contactId, + content: EncryptedContent( contactRequest: EncryptedContent_ContactRequest( type: EncryptedContent_ContactRequest_Type.REQUEST, ), - ), + ).writeToBuffer(), + onlySendIfNoReceiptsAreOpen: false, + onlyReturnEncryptedData: false, + blocking: true, ); if (context.mounted) { showSnackbar( diff --git a/lib/src/visual/views/settings/developer/automated_testing.view.dart b/lib/src/visual/views/settings/developer/automated_testing.view.dart index adaa8d66..a30ac4cd 100644 --- a/lib/src/visual/views/settings/developer/automated_testing.view.dart +++ b/lib/src/visual/views/settings/developer/automated_testing.view.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -64,10 +63,9 @@ class _AutomatedTestingViewState extends State { final serializedSession = originalSession.serialize(); for (var i = 0; i < 10; i++) { - await insertAndSendTextMessage( - group.groupId, - 'DesyncTest_1', - null, + await RustApi.insertAndSendText( + groupId: group.groupId, + text: 'DesyncTest_1', ); } @@ -79,10 +77,9 @@ class _AutomatedTestingViewState extends State { corruptedSession, ); - await insertAndSendTextMessage( - group.groupId, - 'DesyncTest_2', - null, + await RustApi.insertAndSendText( + groupId: group.groupId, + text: 'DesyncTest_2', ); // The other client should res @@ -110,10 +107,9 @@ class _AutomatedTestingViewState extends State { lotsOfMessagesStatus = 'At message $i to ${contact.username}.'; }); - await insertAndSendTextMessage( - group!.groupId, - 'Message $i.', - null, + await RustApi.insertAndSendText( + groupId: group!.groupId, + text: 'Message $i.', ); } } diff --git a/rust/src/api/messages/incoming/mod.rs b/rust/src/api/messages/incoming.rs similarity index 58% rename from rust/src/api/messages/incoming/mod.rs rename to rust/src/api/messages/incoming.rs index 502ce8f7..86dc3e20 100644 --- a/rust/src/api/messages/incoming/mod.rs +++ b/rust/src/api/messages/incoming.rs @@ -3,9 +3,21 @@ * */ -pub mod client2client; +mod additional_data; +pub(crate) mod contact; +mod errors; +mod groups; +mod media; +pub mod messages; +mod reaction; +pub mod recovery; +mod text_message; +mod typing_indicator; +mod user_discovery; +mod verification; -use crate::api::messages::incoming::client2client::messages::{ +use crate::api::messages::content_type_kind; +use crate::api::messages::incoming::messages::{ decrypt_legacy_signal_with_error, ensure_contact_exists, handle_plaintext_content, handle_sender_delivery_receipt, process_encrypted_or_queue_error, queue_decryption_error, queue_sender_delivery_receipt, retransmit_queued_receipts, spawn_receipt_delivery, @@ -15,13 +27,14 @@ use crate::api::proto::server_to_client::NewMessage; use crate::api::proto::{client_to_server, server_to_client}; use crate::bridge::callbacks::get_callbacks; use crate::context::Context; -use crate::database::app::tables::Receipt; +use crate::database::app::tables::{Contact, Group, Receipt}; use crate::error::{Result, TwonlyError}; use crate::sealed_sender::SealedSender; use client_to_server::response::{ok, Response}; use prost::Message as _; use proto::message::Type; use server_to_client::v0::Kind; +use sqlx::{Sqlite, Transaction}; use std::sync::Arc; pub(crate) async fn handle_server_message( @@ -162,25 +175,29 @@ pub(crate) async fn handle_decoded_server_message( let database = ctx.app_db.read().await.clone(); - let mut tr = database.pool.begin().await?; + let mut t = database.pool.begin().await?; - let claimed = Receipt::claim_received(&mut tr, &message.receipt_id).await?; + let claimed = Receipt::claim_received(&mut t, &message.receipt_id).await?; if !claimed { // Delivery receipts are terminal messages and must never themselves be // acknowledged. For regular messages Dart retries the delivery receipt // after ten days, atomically claiming the retry by moving created_at. let should_resend = message_type != Type::SenderDeliveryReceipt - && Receipt::claim_received_retry(&mut tr, &message.receipt_id).await?; + && Receipt::claim_received_retry(&mut t, &message.receipt_id).await?; + if should_resend { tracing::info!("Queueing sender delivery receipt for retry"); - queue_sender_delivery_receipt(&mut tr, from_user_id, &message.receipt_id).await?; + queue_sender_delivery_receipt(&mut t, from_user_id, &message.receipt_id).await?; } - tr.commit().await?; + + t.commit().await?; + if should_resend { tracing::info!("Spawning receipt delivery for retry"); spawn_receipt_delivery(ctx, message.receipt_id); } + tracing::info!("Returning early because receipt was already claimed"); return Ok(()); } @@ -190,7 +207,7 @@ pub(crate) async fn handle_decoded_server_message( match message_type { Type::SenderDeliveryReceipt => { tracing::info!("Received sender delivery receipt"); - handle_sender_delivery_receipt(&mut tr, from_user_id, &message.receipt_id).await?; + handle_sender_delivery_receipt(&mut t, from_user_id, &message.receipt_id).await?; } Type::Ciphertext | Type::PrekeyBundle => { let ciphertext = message.encrypted_content.ok_or_else(|| { @@ -203,7 +220,7 @@ pub(crate) async fn handle_decoded_server_message( tracing::info!("Decrypted successfully, processing..."); sends_error_response = process_encrypted_or_queue_error( ctx, - &mut tr, + &mut t, from_user_id, &message.receipt_id, content, @@ -221,7 +238,7 @@ pub(crate) async fn handle_decoded_server_message( (callbacks.api.resync_signal_session)(from_user_id).await; } } - queue_decryption_error(&mut tr, from_user_id, &message.receipt_id, error_type) + queue_decryption_error(&mut t, from_user_id, &message.receipt_id, error_type) .await?; sends_error_response = true; } @@ -249,7 +266,7 @@ pub(crate) async fn handle_decoded_server_message( })?; sends_error_response = process_encrypted_or_queue_error( ctx, - &mut tr, + &mut t, from_user_id, &message.receipt_id, content, @@ -262,7 +279,7 @@ pub(crate) async fn handle_decoded_server_message( receipt_id = message.receipt_id, "V2 decryption failed: {error}" ); - queue_decryption_error(&mut tr, from_user_id, &message.receipt_id, 0).await?; + queue_decryption_error(&mut t, from_user_id, &message.receipt_id, 0).await?; sends_error_response = true; } } @@ -272,17 +289,17 @@ pub(crate) async fn handle_decoded_server_message( let plaintext = message.plaintext_content.ok_or_else(|| { TwonlyError::Generic("plaintext client message has no content".into()) })?; - handle_plaintext_content(ctx, &mut tr, from_user_id, &message.receipt_id, plaintext) + handle_plaintext_content(ctx, &mut t, from_user_id, &message.receipt_id, plaintext) .await?; } Type::TestNotification => {} } if is_encrypted_message & !sends_error_response { - queue_sender_delivery_receipt(&mut tr, from_user_id, &message.receipt_id).await?; + queue_sender_delivery_receipt(&mut t, from_user_id, &message.receipt_id).await?; } - tr.commit().await?; + t.commit().await?; database.notify_committed([ "received_receipts", @@ -303,3 +320,187 @@ pub(crate) async fn handle_decoded_server_message( Ok(()) } + +/// Dispatches already-decrypted client-to-client content to its concrete +/// feature module. Transport decoding and Signal decryption do not belong in +/// this dispatcher. +pub(crate) async fn handle_encrypted( + ctx: &Arc, + t: &mut Transaction<'_, Sqlite>, + from_user_id: i64, + receipt_id: &str, + content: proto::EncryptedContent, +) -> Result<()> { + Receipt::mark_all_for_retry(t, from_user_id).await?; + + if let Some(version) = content.sender_user_discovery_version.clone() { + user_discovery::check_sender_version(ctx, t, from_user_id, version).await?; + } + + contact::check_for_profile_update(t, from_user_id, &content).await?; + + if content.ask_for_friend_promotions == Some(true) { + Contact::update_ask_for_friend_promotions(t, from_user_id).await?; + } + + let type_kind = content_type_kind(&content); + + tracing::Span::current().record("kind", type_kind); + tracing::info!("Handling incoming message: {type_kind}"); + + if let Some(request) = content.contact_request { + return contact::handle_contact_request(ctx, t, from_user_id, request).await; + } + + if let Some(update) = content.contact_update { + return contact::handle_contact_update( + ctx, + t, + from_user_id, + content.sender_profile_counter, + update, + ) + .await; + } + + if let Some(update) = content.message_update { + return text_message::handle_message_update(ctx, t, from_user_id, update).await; + } + + if let Some(update) = content.media_update { + return media::handle_media_update(t, from_user_id, update).await; + } + + if let Some(error) = content.error_messages { + return errors::handle_error_message( + ctx, + t, + from_user_id, + content.group_id.as_deref(), + error, + ) + .await; + } + + if let Some(update) = content.user_discovery_update { + return user_discovery::handle_user_discovery_update(ctx, t, from_user_id, update).await; + } + + if let Some(request) = content.user_discovery_request { + return user_discovery::handle_user_discovery_request(ctx, t, from_user_id, request).await; + } + + if let Some(proof) = content.key_verification_proof { + return verification::handle_key_verification_proof(t, from_user_id, proof).await; + } + + if let Some(recovery) = content.passwordless_recovery { + return recovery::handle_passwordless_recovery(t, from_user_id, recovery).await; + } + + if let Some(heartbeat) = content.passwordless_recovery_heartbeat { + return recovery::handle_passwordless_recovery_heartbeat(t, from_user_id, heartbeat).await; + } + + let group_id = content + .group_id + .ok_or_else(|| TwonlyError::Generic("group-scoped message has no group ID".into()))?; + + if let Some(create) = content.group_create { + return groups::handle_group_create(ctx, t, from_user_id, &group_id, create).await; + } + + if let Some(join) = content.group_join { + return groups::handle_group_join(t, from_user_id, &group_id, join).await; + } + + let is_member = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM group_members WHERE group_id = ? AND contact_id = ?)", + group_id, + from_user_id, + ) + .fetch_one(&mut **t) + .await? + != 0; + + if !is_member { + let local_user_id = ctx.user_id().await?; + + if Group::direct_chat_id(local_user_id, from_user_id) == group_id { + let contact = Contact::get_contact_by_id(t, from_user_id).await?; + + if let Some(contact) = + contact.filter(|value| value.accepted != 0 && value.deleted_by_user == 0) + { + Group::create_direct_chat(ctx, t, contact).await?; + } else { + sqlx::query!( + "UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?", + from_user_id, + ) + .execute(&mut **t) + .await?; + messages::queue_encrypted_content( + t, + from_user_id, + proto::EncryptedContent { + error_messages: Some(proto::encrypted_content::ErrorMessages { + r#type: proto::encrypted_content::error_messages::Type::ErrorProcessingMessageCreatedAccountRequestInstead as i32, + related_receipt_id: receipt_id.to_owned(), + }), + ..Default::default() + }, + false, + ) + .await?; + return Ok(()); + } + } + } + + groups::ensure_group_member(t, from_user_id, &group_id).await?; + + if content.resend_group_public_key.is_some() { + return groups::handle_resend_group_public_key(t, from_user_id, &group_id).await; + } + + if let Some(update) = content.group_update { + return groups::handle_group_update(ctx, t, from_user_id, &group_id, update).await; + } + + if let Some(flame) = content.flame_sync { + return groups::handle_flame_sync(t, &group_id, flame).await; + } + + if let Some(message) = content.text_message { + return text_message::handle_text_message(t, from_user_id, &group_id, message).await; + } + + if let Some(message) = content.additional_data_message { + return additional_data::handle_additional_data_message( + ctx, + t, + from_user_id, + &group_id, + message, + ) + .await; + } + + if let Some(media) = content.media { + return media::handle_media(t, from_user_id, &group_id, media).await; + } + + if let Some(reaction) = content.reaction { + return reaction::handle_reaction(t, from_user_id, &group_id, reaction).await; + } + + if let Some(indicator) = content.typing_indicator { + return typing_indicator::handle_typing_indicator(t, from_user_id, &group_id, indicator) + .await; + } + + Err(TwonlyError::Generic(format!( + "client2client content in receipt {receipt_id} is not implemented in Rust" + ))) +} diff --git a/rust/src/api/messages/incoming/client2client/additional_data.rs b/rust/src/api/messages/incoming/additional_data.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/additional_data.rs rename to rust/src/api/messages/incoming/additional_data.rs diff --git a/rust/src/api/messages/incoming/client2client/mod.rs b/rust/src/api/messages/incoming/client2client/mod.rs deleted file mode 100644 index 368f310d..00000000 --- a/rust/src/api/messages/incoming/client2client/mod.rs +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2026, Tobias Müller git@tsmr.eu - * - */ - -use crate::api::messages::content_type_kind; -use crate::api::proto::client as proto; -use crate::context::Context; -use crate::database::app::tables::{Contact, Group, Receipt}; -use crate::error::{Result, TwonlyError}; -use sqlx::{Sqlite, Transaction}; -use std::sync::Arc; -use typing_indicator::handle_typing_indicator; - -mod additional_data; -pub(crate) mod contact; -mod errors; -mod groups; -mod media; -pub mod messages; -mod reaction; -pub mod recovery; -mod text_message; -mod typing_indicator; -mod user_discovery; -mod verification; - -/// Dispatches already-decrypted client-to-client content to its concrete -/// feature module. Transport decoding and Signal decryption do not belong in -/// this dispatcher. -pub(crate) async fn handle_encrypted( - ctx: &Arc, - t: &mut Transaction<'_, Sqlite>, - from_user_id: i64, - receipt_id: &str, - content: proto::EncryptedContent, -) -> Result<()> { - Receipt::mark_all_for_retry(t, from_user_id).await?; - - if let Some(version) = content.sender_user_discovery_version.clone() { - user_discovery::check_sender_version(ctx, t, from_user_id, version).await?; - } - - contact::check_for_profile_update(t, from_user_id, &content).await?; - - if content.ask_for_friend_promotions == Some(true) { - Contact::update_ask_for_friend_promotions(t, from_user_id).await?; - } - - let type_kind = content_type_kind(&content); - - tracing::Span::current().record("kind", type_kind); - tracing::info!("Handling incoming message: {type_kind}"); - - if let Some(request) = content.contact_request { - return contact::handle_contact_request(ctx, t, from_user_id, request).await; - } - - if let Some(update) = content.contact_update { - return contact::handle_contact_update( - ctx, - t, - from_user_id, - content.sender_profile_counter, - update, - ) - .await; - } - - if let Some(update) = content.message_update { - return text_message::handle_message_update(ctx, t, from_user_id, update).await; - } - - if let Some(update) = content.media_update { - return media::handle_media_update(t, from_user_id, update).await; - } - - if let Some(error) = content.error_messages { - return errors::handle_error_message( - ctx, - t, - from_user_id, - content.group_id.as_deref(), - error, - ) - .await; - } - - if let Some(update) = content.user_discovery_update { - return user_discovery::handle_user_discovery_update(ctx, t, from_user_id, update).await; - } - - if let Some(request) = content.user_discovery_request { - return user_discovery::handle_user_discovery_request(ctx, t, from_user_id, request).await; - } - - if let Some(proof) = content.key_verification_proof { - return verification::handle_key_verification_proof(t, from_user_id, proof).await; - } - - if let Some(recovery) = content.passwordless_recovery { - return recovery::handle_passwordless_recovery(t, from_user_id, recovery).await; - } - - if let Some(heartbeat) = content.passwordless_recovery_heartbeat { - return recovery::handle_passwordless_recovery_heartbeat(t, from_user_id, heartbeat).await; - } - - let group_id = content - .group_id - .ok_or_else(|| TwonlyError::Generic("group-scoped message has no group ID".into()))?; - - if let Some(create) = content.group_create { - return groups::handle_group_create(ctx, t, from_user_id, &group_id, create).await; - } - - if let Some(join) = content.group_join { - return groups::handle_group_join(t, from_user_id, &group_id, join).await; - } - - let is_member = sqlx::query_scalar!( - "SELECT EXISTS(SELECT 1 FROM group_members WHERE group_id = ? AND contact_id = ?)", - group_id, - from_user_id, - ) - .fetch_one(&mut **t) - .await? - != 0; - - if !is_member { - let local_user_id = ctx.user_id().await?; - - if Group::direct_chat_id(local_user_id, from_user_id) == group_id { - let contact = Contact::get_contact_by_id(t, from_user_id).await?; - - if let Some(contact) = - contact.filter(|value| value.accepted != 0 && value.deleted_by_user == 0) - { - Group::create_direct_chat(ctx, t, contact).await?; - } else { - sqlx::query!( - "UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?", - from_user_id, - ) - .execute(&mut **t) - .await?; - messages::queue_encrypted_content( - t, - from_user_id, - proto::EncryptedContent { - error_messages: Some(proto::encrypted_content::ErrorMessages { - r#type: proto::encrypted_content::error_messages::Type::ErrorProcessingMessageCreatedAccountRequestInstead as i32, - related_receipt_id: receipt_id.to_owned(), - }), - ..Default::default() - }, - false, - ) - .await?; - return Ok(()); - } - } - } - - groups::ensure_group_member(t, from_user_id, &group_id).await?; - - if content.resend_group_public_key.is_some() { - return groups::handle_resend_group_public_key(t, from_user_id, &group_id).await; - } - - if let Some(update) = content.group_update { - return groups::handle_group_update(ctx, t, from_user_id, &group_id, update).await; - } - - if let Some(flame) = content.flame_sync { - return groups::handle_flame_sync(t, &group_id, flame).await; - } - - if let Some(message) = content.text_message { - return text_message::handle_text_message(t, from_user_id, &group_id, message).await; - } - - if let Some(message) = content.additional_data_message { - return additional_data::handle_additional_data_message( - ctx, - t, - from_user_id, - &group_id, - message, - ) - .await; - } - - if let Some(media) = content.media { - return media::handle_media(t, from_user_id, &group_id, media).await; - } - - if let Some(reaction) = content.reaction { - return reaction::handle_reaction(t, from_user_id, &group_id, reaction).await; - } - - if let Some(indicator) = content.typing_indicator { - return handle_typing_indicator(t, from_user_id, &group_id, indicator).await; - } - - Err(TwonlyError::Generic(format!( - "client2client content in receipt {receipt_id} is not implemented in Rust" - ))) -} diff --git a/rust/src/api/messages/incoming/client2client/contact.rs b/rust/src/api/messages/incoming/contact.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/contact.rs rename to rust/src/api/messages/incoming/contact.rs diff --git a/rust/src/api/messages/incoming/client2client/errors.rs b/rust/src/api/messages/incoming/errors.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/errors.rs rename to rust/src/api/messages/incoming/errors.rs diff --git a/rust/src/api/messages/incoming/client2client/groups.rs b/rust/src/api/messages/incoming/groups.rs similarity index 98% rename from rust/src/api/messages/incoming/client2client/groups.rs rename to rust/src/api/messages/incoming/groups.rs index 76d033ff..3baecbf5 100644 --- a/rust/src/api/messages/incoming/client2client/groups.rs +++ b/rust/src/api/messages/incoming/groups.rs @@ -5,7 +5,7 @@ use std::sync::Arc; -use crate::api::messages::incoming::client2client::messages::queue_encrypted_content; +use crate::api::messages::incoming::messages::queue_encrypted_content; use crate::api::proto::client::encrypted_content; use crate::context::Context; use crate::database::app::tables::{Contact, Group}; diff --git a/rust/src/api/messages/incoming/client2client/media.rs b/rust/src/api/messages/incoming/media.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/media.rs rename to rust/src/api/messages/incoming/media.rs diff --git a/rust/src/api/messages/incoming/client2client/messages.rs b/rust/src/api/messages/incoming/messages.rs similarity index 98% rename from rust/src/api/messages/incoming/client2client/messages.rs rename to rust/src/api/messages/incoming/messages.rs index 700a35f8..e5b278ba 100644 --- a/rust/src/api/messages/incoming/client2client/messages.rs +++ b/rust/src/api/messages/incoming/messages.rs @@ -352,8 +352,6 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc, receipt_id: &str) -> let message_type = proto::message::Type::try_from(message.r#type)?; - let push_data: Option> = None; - match message_type { proto::message::Type::Ciphertext | proto::message::Type::PrekeyBundle => { let plaintext = message.encrypted_content.take().ok_or_else(|| { @@ -375,8 +373,7 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc, receipt_id: &str) -> _ => {} } - match Server::send_text_message(ctx, row.contact_id, message.encode_to_vec(), push_data).await? - { + match Server::send_text_message(ctx, row.contact_id, message.encode_to_vec()).await? { ServerResult::Ok(()) => {} ServerResult::ErrorCode(code) => { return Err(TwonlyError::Generic(format!( @@ -426,7 +423,7 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc, receipt_id: &str) -> pub(crate) async fn prepare_queued_receipt( ctx: &Arc, receipt_id: &str, -) -> Result, Option>)>> { +) -> Result>> { let database = ctx.app_db.read().await.clone(); let row = sqlx::query!( r#"SELECT contact_id, message, message_id, retry_count @@ -439,7 +436,6 @@ pub(crate) async fn prepare_queued_receipt( let mut message = proto::Message::decode(row.message.as_slice()) .map_err(|error| TwonlyError::Generic(format!("invalid queued message: {error}")))?; message.receipt_id = receipt_id.to_owned(); - let push_data: Option> = None; match proto::message::Type::try_from(message.r#type) .map_err(|_| TwonlyError::Generic("queued message has invalid type".into()))? @@ -467,7 +463,7 @@ pub(crate) async fn prepare_queued_receipt( } _ => {} } - Ok(Some((message.encode_to_vec(), push_data))) + Ok(Some(message.encode_to_vec())) } pub async fn retransmit_queued_receipts(ctx: &Arc) -> Result<()> { @@ -579,6 +575,7 @@ pub(crate) async fn handle_sender_delivery_receipt( .fetch_optional(&mut **transaction) .await? .flatten(); + if let Some(message_id) = message_id { sqlx::query!( r#" @@ -605,6 +602,7 @@ pub(crate) async fn handle_sender_delivery_receipt( ) .execute(&mut **transaction) .await?; + Ok(()) } diff --git a/rust/src/api/messages/incoming/client2client/reaction.rs b/rust/src/api/messages/incoming/reaction.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/reaction.rs rename to rust/src/api/messages/incoming/reaction.rs diff --git a/rust/src/api/messages/incoming/client2client/recovery.rs b/rust/src/api/messages/incoming/recovery.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/recovery.rs rename to rust/src/api/messages/incoming/recovery.rs diff --git a/rust/src/api/messages/incoming/client2client/text_message.rs b/rust/src/api/messages/incoming/text_message.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/text_message.rs rename to rust/src/api/messages/incoming/text_message.rs diff --git a/rust/src/api/messages/incoming/client2client/typing_indicator.rs b/rust/src/api/messages/incoming/typing_indicator.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/typing_indicator.rs rename to rust/src/api/messages/incoming/typing_indicator.rs diff --git a/rust/src/api/messages/incoming/client2client/user_discovery.rs b/rust/src/api/messages/incoming/user_discovery.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/user_discovery.rs rename to rust/src/api/messages/incoming/user_discovery.rs diff --git a/rust/src/api/messages/incoming/client2client/verification.rs b/rust/src/api/messages/incoming/verification.rs similarity index 100% rename from rust/src/api/messages/incoming/client2client/verification.rs rename to rust/src/api/messages/incoming/verification.rs diff --git a/rust/src/api/messages/mod.rs b/rust/src/api/messages/mod.rs index bd8f9abc..804d9bde 100644 --- a/rust/src/api/messages/mod.rs +++ b/rust/src/api/messages/mod.rs @@ -4,7 +4,6 @@ */ pub mod incoming; -#[doc(hidden)] pub mod outgoing; pub(crate) fn content_type_kind( diff --git a/rust/src/api/messages/outgoing.rs b/rust/src/api/messages/outgoing.rs index 9d56452d..c74a6efd 100644 --- a/rust/src/api/messages/outgoing.rs +++ b/rust/src/api/messages/outgoing.rs @@ -4,7 +4,7 @@ */ use crate::api::messages::content_type_kind; -use crate::api::messages::incoming::client2client::messages; +use crate::api::messages::incoming::messages; use crate::api::proto::client as proto; use crate::context::Context; use crate::error::Result; @@ -48,13 +48,8 @@ pub(crate) async fn decorate_content( .fetch_one(&database.pool) .await?; if allowed != 0 { - content.sender_user_discovery_version = Some( - ctx.user_discovery - .get() - .await - .get_current_version() - .await?, - ); + content.sender_user_discovery_version = + Some(ctx.user_discovery.get().await.get_current_version().await?); } } Ok(()) @@ -69,7 +64,7 @@ pub async fn send_c2c_message_to_contact( #[builder(default)] only_send_if_no_receipts_are_open: bool, #[builder(default)] only_return_encrypted_data: bool, #[builder(default)] blocking: bool, -) -> Result, Option>)>> { +) -> Result>> { let mut content = proto::EncryptedContent::decode(encrypted_content.as_slice())?; decorate_content(ctx, contact_id, &mut content, message_id.is_some()).await?; diff --git a/rust/src/api/runtime/helpers.rs b/rust/src/api/runtime/helpers.rs index 0666d8b2..a97cac54 100644 --- a/rust/src/api/runtime/helpers.rs +++ b/rust/src/api/runtime/helpers.rs @@ -3,7 +3,7 @@ * */ -use crate::api::messages::incoming::client2client::{messages, recovery}; +use crate::api::messages::incoming::{messages, recovery}; use crate::api::proto::server_to_client; use crate::api::runtime::ApiRuntime; use crate::api::Server; diff --git a/rust/src/api/server/contacts.rs b/rust/src/api/server/contacts.rs index 3208caa1..4377bedc 100644 --- a/rust/src/api/server/contacts.rs +++ b/rust/src/api/server/contacts.rs @@ -136,7 +136,6 @@ impl Server { ctx: &Arc, user_id: i64, body: Vec, - push_data: Option>, ) -> Result> { server_ok!( Self::application_for_contact( @@ -145,7 +144,7 @@ impl Server { client_to_server::application_data::TextMessage { user_id, body, - push_data, + push_data: None, }, ), user_id, diff --git a/rust/src/bridge/api.rs b/rust/src/bridge/api.rs index 77c1e21e..aafc3c23 100644 --- a/rust/src/bridge/api.rs +++ b/rust/src/bridge/api.rs @@ -3,7 +3,7 @@ * */ -use crate::api::messages::incoming::client2client::messages; +use crate::api::messages::incoming::messages; use crate::api::proto::server_to_client::response::ok::Ok as ResponseOk; use crate::api::runtime::helpers::decode_ok_value; use crate::api::ApiRuntime; @@ -104,11 +104,6 @@ pub enum ApiEventKind { /// Flutter-facing facade for the Rust-owned API runtime. pub struct RustApi {} -pub struct PreparedOutgoingMessage { - pub message: Vec, - pub push_data: Option>, -} - impl RustApi { pub async fn request_contact_by_username(username: String) -> Result<()> { let ctx = Context::get_static()?; @@ -334,7 +329,7 @@ impl RustApi { } pub async fn perform_passwordless_recovery_heartbeat() -> Result<()> { let ctx = Context::get_static()?; - crate::api::messages::incoming::client2client::recovery::perform_heartbeat(ctx).await + crate::api::messages::incoming::recovery::perform_heartbeat(ctx).await } pub async fn get_server_key_for_passwordless_recovery( user_id: i64, @@ -459,13 +454,9 @@ impl RustApi { .await .and_then(empty_api_response) } - pub async fn send_text_message( - user_id: i64, - body: Vec, - push_data: Option>, - ) -> Result<()> { + pub async fn send_text_message(user_id: i64, body: Vec) -> Result<()> { let ctx = Context::get_static()?; - Server::send_text_message(ctx, user_id, body, push_data) + Server::send_text_message(ctx, user_id, body) .await .and_then(api_result) } @@ -477,21 +468,19 @@ impl RustApi { only_send_if_no_receipts_are_open: bool, only_return_encrypted_data: bool, blocking: bool, - ) -> Result> { + ) -> Result>> { let ctx = Context::get_static()?; - Ok( - crate::api::messages::outgoing::send_c2c_message_to_contact() - .ctx(ctx) - .contact_id(contact_id) - .encrypted_content(content) - .maybe_message_id(message_id) - .only_send_if_no_receipts_are_open(only_send_if_no_receipts_are_open) - .only_return_encrypted_data(only_return_encrypted_data) - .blocking(blocking) - .call() - .await? - .map(|(message, push_data)| PreparedOutgoingMessage { message, push_data }), - ) + crate::api::messages::outgoing::send_c2c_message_to_contact() + .ctx(ctx) + .contact_id(contact_id) + .encrypted_content(content) + .maybe_message_id(message_id) + .only_send_if_no_receipts_are_open(only_send_if_no_receipts_are_open) + .only_return_encrypted_data(only_return_encrypted_data) + .blocking(blocking) + .call() + .await + .map_err(Into::into) } pub async fn send_encrypted_content_to_group( @@ -574,14 +563,10 @@ impl RustApi { .await } - pub async fn prepare_queued_message( - receipt_id: String, - ) -> Result> { - Ok( - messages::prepare_queued_receipt(Context::get_static()?, &receipt_id) - .await? - .map(|(message, push_data)| PreparedOutgoingMessage { message, push_data }), - ) + pub async fn prepare_queued_message(receipt_id: String) -> Result>> { + messages::prepare_queued_receipt(Context::get_static()?, &receipt_id) + .await + .map_err(Into::into) } pub async fn notify_messages_opened(contact_id: i64, message_ids: Vec) -> Result<()> { diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index e21b0fab..bb9beefc 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -2536,17 +2536,13 @@ fn wire__crate__bridge__api__rust_api_send_text_message_impl( flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_user_id = ::sse_decode(&mut deserializer); let api_body = >::sse_decode(&mut deserializer); - let api_push_data = >>::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( (move || async move { - let output_ok = crate::bridge::api::RustApi::send_text_message( - api_user_id, - api_body, - api_push_data, - ) - .await?; + let output_ok = + crate::bridge::api::RustApi::send_text_message(api_user_id, api_body) + .await?; Ok(output_ok) })() .await, @@ -4952,19 +4948,6 @@ impl SseDecode for Option { } } -impl SseDecode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - if (::sse_decode(deserializer)) { - return Some(::sse_decode( - deserializer, - )); - } else { - return None; - } - } -} - impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -5064,18 +5047,6 @@ impl SseDecode for crate::api::server::prekeys::PqcPreKeyInput { } } -impl SseDecode for crate::bridge::api::PreparedOutgoingMessage { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_message = >::sse_decode(deserializer); - let mut var_pushData = >>::sse_decode(deserializer); - return crate::bridge::api::PreparedOutgoingMessage { - message: var_message, - push_data: var_pushData, - }; - } -} - impl SseDecode for (i64, Vec) { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -5927,27 +5898,6 @@ impl flutter_rust_bridge::IntoIntoDart flutter_rust_bridge::for_generated::DartAbi { - [ - self.message.into_into_dart().into_dart(), - self.push_data.into_into_dart().into_dart(), - ] - .into_dart() - } -} -impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive - for crate::bridge::api::PreparedOutgoingMessage -{ -} -impl flutter_rust_bridge::IntoIntoDart - for crate::bridge::api::PreparedOutgoingMessage -{ - fn into_into_dart(self) -> crate::bridge::api::PreparedOutgoingMessage { - self - } -} -// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::bridge::api::RustApi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { Vec::::new().into_dart() @@ -6779,16 +6729,6 @@ impl SseEncode for Option { } } -impl SseEncode for Option { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.is_some(), serializer); - if let Some(value) = self { - ::sse_encode(value, serializer); - } - } -} - impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6866,14 +6806,6 @@ impl SseEncode for crate::api::server::prekeys::PqcPreKeyInput { } } -impl SseEncode for crate::bridge::api::PreparedOutgoingMessage { - // Codec=Sse (Serialization based), see doc to use other codecs - fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - >::sse_encode(self.message, serializer); - >>::sse_encode(self.push_data, serializer); - } -} - impl SseEncode for (i64, Vec) { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/rust/src/services/groups/mod.rs b/rust/src/services/groups/mod.rs index d4d94be9..258206f4 100644 --- a/rust/src/services/groups/mod.rs +++ b/rust/src/services/groups/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod crypto; pub(crate) mod model; use crate::api::groups::GroupApi; -use crate::api::messages::incoming::client2client::messages::queue_encrypted_content; +use crate::api::messages::incoming::messages::queue_encrypted_content; use crate::api::messages::outgoing::send_c2c_message_to_contact; use crate::api::proto::client::encrypted_content::GroupJoin; use crate::api::proto::client::{ diff --git a/rust/src/services/messages.rs b/rust/src/services/messages.rs index 44b62425..cfd94628 100644 --- a/rust/src/services/messages.rs +++ b/rust/src/services/messages.rs @@ -3,7 +3,7 @@ * */ -use crate::api::messages::incoming::client2client::messages::{ +use crate::api::messages::incoming::messages::{ self, queue_encrypted_content, send_queued_receipt, }; use crate::api::messages::outgoing::{decorate_content, send_c2c_message_to_contact}; diff --git a/rust/tests/api/group_resilience.rs b/rust/tests/api/group_resilience.rs index 56ffd07b..14fb4bca 100644 --- a/rust/tests/api/group_resilience.rs +++ b/rust/tests/api/group_resilience.rs @@ -84,7 +84,7 @@ async fn test_group_membership_error_healing() -> anyhow::Result<()> { // Wait a brief moment for group join to be acknowledged, then ensure queued receipts are retransmitted tokio::time::sleep(std::time::Duration::from_millis(500)).await; - let _ = rust_lib_twonly::api::messages::incoming::client2client::messages::retransmit_queued_receipts(&tester_a.context).await; + let _ = rust_lib_twonly::api::messages::incoming::messages::retransmit_queued_receipts(&tester_a.context).await; tester_b .wait_for_text_message(&msg_id, tester_a.user_id, "Message triggering heal") diff --git a/rust/tests/api/recovery.rs b/rust/tests/api/recovery.rs index 2b97db6d..63b4ba3d 100644 --- a/rust/tests/api/recovery.rs +++ b/rust/tests/api/recovery.rs @@ -1,6 +1,6 @@ use super::Tester; use prost::Message as _; -use rust_lib_twonly::api::messages::incoming::client2client::recovery::perform_heartbeat; +use rust_lib_twonly::api::messages::incoming::recovery::perform_heartbeat; use rust_lib_twonly::api::messages::outgoing::send_c2c_message_to_contact; use rust_lib_twonly::api::proto::client::{self as proto, encrypted_content}; use rust_lib_twonly::bridge::api::ApiConnectionState;