moving files

This commit is contained in:
otsmr 2026-08-29 01:30:23 +02:00
parent 4557919065
commit da9752d317
51 changed files with 459 additions and 915 deletions

View file

@ -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<void> initializeTwonlyFlutter({required InitConfig config}) =>
RustLib.instance.api.crateBridgeInitializeTwonlyFlutter(config: config);

View file

@ -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,9 +234,8 @@ class RustApi {
.api
.crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat();
static Future<PreparedOutgoingMessage?> prepareQueuedMessage({
required String receiptId,
}) => RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage(
static Future<Uint8List?> prepareQueuedMessage({required String receiptId}) =>
RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage(
receiptId: receiptId,
);
@ -348,7 +326,7 @@ class RustApi {
contactId: contactId,
);
static Future<PreparedOutgoingMessage?> sendEncryptedContent({
static Future<Uint8List?> sendEncryptedContent({
required PlatformInt64 contactId,
required List<int> content,
String? messageId,
@ -384,11 +362,9 @@ class RustApi {
static Future<void> sendTextMessage({
required PlatformInt64 userId,
required List<int> body,
Uint8List? pushData,
}) => RustLib.instance.api.crateBridgeApiRustApiSendTextMessage(
userId: userId,
body: body,
pushData: pushData,
);
static Future<void> sendTyping({

View file

@ -292,7 +292,7 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat();
Future<PreparedOutgoingMessage?> crateBridgeApiRustApiPrepareQueuedMessage({
Future<Uint8List?> crateBridgeApiRustApiPrepareQueuedMessage({
required String receiptId,
});
@ -352,7 +352,7 @@ abstract class RustLibApi extends BaseApi {
required PlatformInt64 contactId,
});
Future<PreparedOutgoingMessage?> crateBridgeApiRustApiSendEncryptedContent({
Future<Uint8List?> crateBridgeApiRustApiSendEncryptedContent({
required PlatformInt64 contactId,
required List<int> content,
String? messageId,
@ -375,7 +375,6 @@ abstract class RustLibApi extends BaseApi {
Future<void> crateBridgeApiRustApiSendTextMessage({
required PlatformInt64 userId,
required List<int> body,
Uint8List? pushData,
});
Future<void> crateBridgeApiRustApiSendTyping({
@ -2329,7 +2328,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
@override
Future<PreparedOutgoingMessage?> crateBridgeApiRustApiPrepareQueuedMessage({
Future<Uint8List?> 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<PreparedOutgoingMessage?> crateBridgeApiRustApiSendEncryptedContent({
Future<Uint8List?> crateBridgeApiRustApiSendEncryptedContent({
required PlatformInt64 contactId,
required List<int> 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<void> crateBridgeApiRustApiSendTextMessage({
required PlatformInt64 userId,
required List<int> 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<dynamic>;
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,

View file

@ -150,11 +150,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
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<RustLibWire> {
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<RustLibWire> {
@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<RustLibWire> {
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<RustLibWire> {
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<RustLibWire> {
@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<RustLibWire> {
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<RustLibWire> {
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<RustLibWire> {
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,

View file

@ -152,11 +152,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
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<RustLibWire> {
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<RustLibWire> {
@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<RustLibWire> {
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<RustLibWire> {
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<RustLibWire> {
@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<RustLibWire> {
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<RustLibWire> {
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<RustLibWire> {
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,

View file

@ -691,11 +691,13 @@ Future<void> _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<void> _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);
}

View file

@ -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<Uint8List?> 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<void> insertAndSendTextMessage(
String groupId,
String textMessage,
String? quotesMessageId,
) async {
await rust_api.RustApi.insertAndSendText(
groupId: groupId,
text: textMessage,
quoteMessageId: quotesMessageId,
);
}
Future<void> insertAndSendContactShareMessage(
String groupId,
List<int> contactsToShare,
) async {
await rust_api.RustApi.insertAndSendContactShare(
groupId: groupId,
contactIds: frb.Int64List.fromList(contactsToShare),
);
}
Future<void> insertAndSendAskAboutUserMessage(
int contactId,
int askAboutUserId,
) async {
await rust_api.RustApi.insertAndSendAskAboutUser(
contactId: contactId,
askAboutUserId: askAboutUserId,
);
}
Future<void> 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<void> sendTypingIndication(String groupId, bool isTyping) =>
rust_api.RustApi.sendTyping(groupId: groupId, isTyping: isTyping);
Future<void> notifyContactAboutOpeningMessage(
int contactId,
List<String> messageOtherIds,
) => rust_api.RustApi.notifyMessagesOpened(
contactId: contactId,
messageIds: messageOtherIds,
);
Future<void> sendContactMyProfileData(int contactId) =>
rust_api.RustApi.sendContactProfile(contactId: contactId);

View file

@ -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<void> 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<bool> 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;

View file

@ -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<void> 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<void> syncFlameCounters({String? forceForGroup}) async {
bestFriend: group.groupId == bestFriend.groupId,
forceUpdate: group.groupId == forceForGroup,
),
),
).writeToBuffer(),
onlySendIfNoReceiptsAreOpen: false,
);
await twonlyDB.groupsDao.updateGroup(

View file

@ -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,
);
}

View file

@ -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(

View file

@ -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,31 +22,6 @@ Future<SignalEncryptResult?> signalEncryptMessage(
Uint8List plaintextContent,
) async {
return lockingSignalProtocol.protect<SignalEncryptResult?>(() async {
return _signalEncryptMessageV1(target, plaintextContent);
});
}
Future<SignalEncryptResult?> 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<SignalEncryptResult?> _signalEncryptMessageV1(
int target,
Uint8List plaintextContent,
) async {
try {
final signalStore = (await getSignalStore())!;
final address = getSignalAddress(target);
@ -67,62 +41,10 @@ Future<SignalEncryptResult?> _signalEncryptMessageV1(
return SignalEncryptResult(cipherText.serialize(), type);
} catch (e) {
Log.error('Could not encrypt message (V1) for target $target: $e');
Log.error('Could not encrypt message for target $target: $e');
return null;
}
}
Future<(pb.EncryptedContent?, pb.PlaintextContent_DecryptionErrorMessage_Type?)>
signalDecryptMessageV2(
int fromUserId,
Uint8List encryptedContentRaw, {
Set<int>? 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);
} 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.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,
);
}
}

View file

@ -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)) {

View file

@ -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<ChatMessagesView>
}
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<ChatMessagesView>
openedMessages[contactId]!,
);
unawaited(
notifyContactAboutOpeningMessage(
contactId,
openedMessages[contactId]!,
RustApi.notifyMessagesOpened(
contactId: contactId,
messageIds: openedMessages[contactId]!,
),
);
}

View file

@ -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<AllReactionsView> {
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,
);
}

View file

@ -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<ShareAdditionalView> {
)
as List<int>?;
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(

View file

@ -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<InChatAudioPlayer> {
_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],
);
}
}

View file

@ -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<ChatMediaEntry> {
}
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,

View file

@ -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<void> 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<void> editTextMessage(BuildContext context, Message message) async {
timestamp.millisecondsSinceEpoch,
),
),
),
).writeToBuffer(),
onlySendIfNoReceiptsAreOpen: false,
);
}
if (!context.mounted) return;

View file

@ -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<MessageInput> {
Future<void> _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<MessageInput> {
_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);
}
});
}

View file

@ -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<MediaViewerView> {
markAsOpenMessageIDs = messageIds;
}
await notifyContactAboutOpeningMessage(
currentMessage!.senderId!,
markAsOpenMessageIDs,
await RustApi.notifyMessagesOpened(
contactId: currentMessage!.senderId!,
messageIds: markAsOpenMessageIDs,
);
}
@ -531,14 +530,15 @@ class _MediaViewerViewState extends State<MediaViewerView> {
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<MediaViewerView> {
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();

View file

@ -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<void> 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,
);
}

View file

@ -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(

View file

@ -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,

View file

@ -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<RestoreFlameComp> {
);
await syncFlameCounters(forceForGroup: _groupId);
await sendCipherTextToGroup(
_groupId,
encryptedContent,
await RustApi.sendEncryptedContentToGroup(
groupId: _groupId,
content: encryptedContent.writeToBuffer(),
messageId: message.messageId,
onlySendIfNoReceiptsAreOpen: false,
);
}

View file

@ -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(

View file

@ -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<AutomatedTestingView> {
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<AutomatedTestingView> {
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<AutomatedTestingView> {
lotsOfMessagesStatus =
'At message $i to ${contact.username}.';
});
await insertAndSendTextMessage(
group!.groupId,
'Message $i.',
null,
await RustApi.insertAndSendText(
groupId: group!.groupId,
text: 'Message $i.',
);
}
}

View file

@ -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<Context>,
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"
)))
}

View file

@ -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<Context>,
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"
)))
}

View file

@ -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};

View file

@ -352,8 +352,6 @@ pub(crate) async fn send_queued_receipt(ctx: &Arc<Context>, receipt_id: &str) ->
let message_type = proto::message::Type::try_from(message.r#type)?;
let push_data: Option<Vec<u8>> = 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<Context>, 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<Context>, receipt_id: &str) ->
pub(crate) async fn prepare_queued_receipt(
ctx: &Arc<Context>,
receipt_id: &str,
) -> Result<Option<(Vec<u8>, Option<Vec<u8>>)>> {
) -> Result<Option<Vec<u8>>> {
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<Vec<u8>> = 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<Context>) -> 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(())
}

View file

@ -4,7 +4,6 @@
*/
pub mod incoming;
#[doc(hidden)]
pub mod outgoing;
pub(crate) fn content_type_kind(

View file

@ -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<(Vec<u8>, Option<Vec<u8>>)>> {
) -> Result<Option<Vec<u8>>> {
let mut content = proto::EncryptedContent::decode(encrypted_content.as_slice())?;
decorate_content(ctx, contact_id, &mut content, message_id.is_some()).await?;

View file

@ -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;

View file

@ -136,7 +136,6 @@ impl Server {
ctx: &Arc<Context>,
user_id: i64,
body: Vec<u8>,
push_data: Option<Vec<u8>>,
) -> Result<ServerResult<()>> {
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,

View file

@ -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<u8>,
pub push_data: Option<Vec<u8>>,
}
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<u8>,
push_data: Option<Vec<u8>>,
) -> Result<()> {
pub async fn send_text_message(user_id: i64, body: Vec<u8>) -> 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,9 +468,8 @@ impl RustApi {
only_send_if_no_receipts_are_open: bool,
only_return_encrypted_data: bool,
blocking: bool,
) -> Result<Option<PreparedOutgoingMessage>> {
) -> Result<Option<Vec<u8>>> {
let ctx = Context::get_static()?;
Ok(
crate::api::messages::outgoing::send_c2c_message_to_contact()
.ctx(ctx)
.contact_id(contact_id)
@ -489,9 +479,8 @@ impl RustApi {
.only_return_encrypted_data(only_return_encrypted_data)
.blocking(blocking)
.call()
.await?
.map(|(message, push_data)| PreparedOutgoingMessage { message, push_data }),
)
.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<Option<PreparedOutgoingMessage>> {
Ok(
pub async fn prepare_queued_message(receipt_id: String) -> Result<Option<Vec<u8>>> {
messages::prepare_queued_receipt(Context::get_static()?, &receipt_id)
.await?
.map(|(message, push_data)| PreparedOutgoingMessage { message, push_data }),
)
.await
.map_err(Into::into)
}
pub async fn notify_messages_opened(contact_id: i64, message_ids: Vec<String>) -> Result<()> {

View file

@ -2536,16 +2536,12 @@ fn wire__crate__bridge__api__rust_api_send_text_message_impl(
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_user_id = <i64>::sse_decode(&mut deserializer);
let api_body = <Vec<u8>>::sse_decode(&mut deserializer);
let api_push_data = <Option<Vec<u8>>>::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,
)
let output_ok =
crate::bridge::api::RustApi::send_text_message(api_user_id, api_body)
.await?;
Ok(output_ok)
})()
@ -4952,19 +4948,6 @@ impl SseDecode for Option<crate::user_config::PasswordlessRecoveryConfig> {
}
}
impl SseDecode for Option<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 {
if (<bool>::sse_decode(deserializer)) {
return Some(<crate::bridge::api::PreparedOutgoingMessage>::sse_decode(
deserializer,
));
} else {
return None;
}
}
}
impl SseDecode for Option<crate::user_config::TwonlySafeBackup> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -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 = <Vec<u8>>::sse_decode(deserializer);
let mut var_pushData = <Option<Vec<u8>>>::sse_decode(deserializer);
return crate::bridge::api::PreparedOutgoingMessage {
message: var_message,
push_data: var_pushData,
};
}
}
impl SseDecode for (i64, Vec<u8>) {
// 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<crate::api::server::prekeys::PqcPreKeyInp
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::api::PreparedOutgoingMessage {
fn into_dart(self) -> 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<crate::bridge::api::PreparedOutgoingMessage>
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::<u8>::new().into_dart()
@ -6779,16 +6729,6 @@ impl SseEncode for Option<crate::user_config::PasswordlessRecoveryConfig> {
}
}
impl SseEncode for Option<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) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<crate::bridge::api::PreparedOutgoingMessage>::sse_encode(value, serializer);
}
}
}
impl SseEncode for Option<crate::user_config::TwonlySafeBackup> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -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) {
<Vec<u8>>::sse_encode(self.message, serializer);
<Option<Vec<u8>>>::sse_encode(self.push_data, serializer);
}
}
impl SseEncode for (i64, Vec<u8>) {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {

View file

@ -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::{

View file

@ -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};

View file

@ -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")

View file

@ -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;