move api fully to rust

This commit is contained in:
otsmr 2026-08-28 22:56:28 +02:00
parent b09d79f5d6
commit 63e3eb0f3d
32 changed files with 381 additions and 694 deletions

View file

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart' show SharedFile;
import 'package:provider/provider.dart';
import 'package:twonly/core/bridge/api.dart' as rust_api;
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart';
@ -59,11 +60,16 @@ class _AppState extends State<App> with WidgetsBindingObserver {
if (_wasPaused) {
AppState.isAppInBackground = false;
twonlyDB.markUpdated();
unawaited(apiService.connect());
unawaited(
rust_api.RustApi.setBackground(inBackground: false),
);
}
} else if (state == AppLifecycleState.paused) {
_wasPaused = true;
AppState.isAppInBackground = true;
unawaited(
rust_api.RustApi.setBackground(inBackground: true),
);
}
}

View file

@ -9,7 +9,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_callbacks`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Api`, `FlutterCallbacks`, `LegacySignal`, `Logging`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`
Future<void> initFlutterCallbacks({
required int callbackId,
@ -25,8 +25,6 @@ Future<void> initFlutterCallbacks({
Uint8List,
)
legacySignalEncrypt,
required FutureOr<List<LegacySignalPreKey>> Function()
legacySignalGeneratePrekeys,
required FutureOr<void> Function(PlatformInt64) apiResyncSignalSession,
required FutureOr<void> Function(String, String, PlatformInt64, String)
apiMediaAction,
@ -40,7 +38,6 @@ Future<void> initFlutterCallbacks({
loggingGetStreamSink: loggingGetStreamSink,
legacySignalDecrypt: legacySignalDecrypt,
legacySignalEncrypt: legacySignalEncrypt,
legacySignalGeneratePrekeys: legacySignalGeneratePrekeys,
apiResyncSignalSession: apiResyncSignalSession,
apiMediaAction: apiMediaAction,
apiVerificationProof: apiVerificationProof,
@ -95,24 +92,3 @@ class LegacySignalEncryptResult {
ciphertext == other.ciphertext &&
messageType == other.messageType;
}
class LegacySignalPreKey {
final PlatformInt64 id;
final Uint8List publicKey;
const LegacySignalPreKey({
required this.id,
required this.publicKey,
});
@override
int get hashCode => id.hashCode ^ publicKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is LegacySignalPreKey &&
runtimeType == other.runtimeType &&
id == other.id &&
publicKey == other.publicKey;
}

View file

@ -149,8 +149,6 @@ abstract class RustLibApi extends BaseApi {
Uint8List,
)
legacySignalEncrypt,
required FutureOr<List<LegacySignalPreKey>> Function()
legacySignalGeneratePrekeys,
required FutureOr<void> Function(PlatformInt64) apiResyncSignalSession,
required FutureOr<void> Function(String, String, PlatformInt64, String)
apiMediaAction,
@ -933,8 +931,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
Uint8List,
)
legacySignalEncrypt,
required FutureOr<List<LegacySignalPreKey>> Function()
legacySignalGeneratePrekeys,
required FutureOr<void> Function(PlatformInt64) apiResyncSignalSession,
required FutureOr<void> Function(String, String, PlatformInt64, String)
apiMediaAction,
@ -961,10 +957,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
legacySignalEncrypt,
serializer,
);
sse_encode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
legacySignalGeneratePrekeys,
serializer,
);
sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
apiResyncSignalSession,
serializer,
@ -1006,7 +998,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
loggingGetStreamSink,
legacySignalDecrypt,
legacySignalEncrypt,
legacySignalGeneratePrekeys,
apiResyncSignalSession,
apiMediaAction,
apiVerificationProof,
@ -1027,7 +1018,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
"loggingGetStreamSink",
"legacySignalDecrypt",
"legacySignalEncrypt",
"legacySignalGeneratePrekeys",
"apiResyncSignalSession",
"apiMediaAction",
"apiVerificationProof",
@ -4893,43 +4883,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
};
}
Future<void> Function(
int,
)
encode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
FutureOr<List<LegacySignalPreKey>> Function() raw,
) {
return (
callId,
) async {
Box<List<LegacySignalPreKey>>? rawOutput;
Box<AnyhowException>? rawError;
try {
rawOutput = Box(await raw());
} catch (e, s) {
rawError = Box(AnyhowException("$e\n\n$s"));
}
final serializer = SseSerializer(generalizedFrbRustBinding);
assert((rawOutput != null) ^ (rawError != null));
if (rawOutput != null) {
serializer.buffer.putUint8(0);
sse_encode_list_legacy_signal_pre_key(rawOutput.value, serializer);
} else {
serializer.buffer.putUint8(1);
sse_encode_AnyhowException(rawError!.value, serializer);
}
final output = serializer.intoRaw();
generalizedFrbRustBinding.dartFnDeliverOutput(
callId: callId,
ptr: output.ptr,
rustVecLen: output.rustVecLen,
dataLen: output.dataLen,
);
};
}
Future<void> Function(int, dynamic)
encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
FutureOr<void> Function(PlatformInt64) raw,
@ -5152,15 +5105,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
throw UnimplementedError('');
}
@protected
FutureOr<List<LegacySignalPreKey>> Function()
dco_decode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
dynamic raw,
) {
// Codec=Dco (DartCObject based), see doc to use other codecs
throw UnimplementedError('');
}
@protected
FutureOr<void> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw) {
@ -5511,18 +5455,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
@protected
LegacySignalPreKey dco_decode_legacy_signal_pre_key(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 LegacySignalPreKey(
id: dco_decode_i_64(arr[0]),
publicKey: dco_decode_list_prim_u_8_strict(arr[1]),
);
}
@protected
LegacyTableMigrationCount dco_decode_legacy_table_migration_count(
dynamic raw,
@ -5549,14 +5481,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (raw as List<dynamic>).map(dco_decode_frb_pqc_pre_key).toList();
}
@protected
List<LegacySignalPreKey> dco_decode_list_legacy_signal_pre_key(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return (raw as List<dynamic>)
.map(dco_decode_legacy_signal_pre_key)
.toList();
}
@protected
List<LegacyTableMigrationCount> dco_decode_list_legacy_table_migration_count(
dynamic raw,
@ -6441,16 +6365,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
@protected
LegacySignalPreKey sse_decode_legacy_signal_pre_key(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_id = sse_decode_i_64(deserializer);
var var_publicKey = sse_decode_list_prim_u_8_strict(deserializer);
return LegacySignalPreKey(id: var_id, publicKey: var_publicKey);
}
@protected
LegacyTableMigrationCount sse_decode_legacy_table_migration_count(
SseDeserializer deserializer,
@ -6487,20 +6401,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return ans_;
}
@protected
List<LegacySignalPreKey> sse_decode_list_legacy_signal_pre_key(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
var len_ = sse_decode_i_32(deserializer);
var ans_ = <LegacySignalPreKey>[];
for (var idx_ = 0; idx_ < len_; ++idx_) {
ans_.add(sse_decode_legacy_signal_pre_key(deserializer));
}
return ans_;
}
@protected
List<LegacyTableMigrationCount> sse_decode_list_legacy_table_migration_count(
SseDeserializer deserializer,
@ -7269,21 +7169,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
@protected
void
sse_encode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
FutureOr<List<LegacySignalPreKey>> Function() self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_DartOpaque(
encode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
self,
),
serializer,
);
}
@protected
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
FutureOr<void> Function(PlatformInt64) self,
@ -7697,16 +7582,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_32(self.messageType, serializer);
}
@protected
void sse_encode_legacy_signal_pre_key(
LegacySignalPreKey self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_64(self.id, serializer);
sse_encode_list_prim_u_8_strict(self.publicKey, serializer);
}
@protected
void sse_encode_legacy_table_migration_count(
LegacyTableMigrationCount self,
@ -7738,18 +7613,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
void sse_encode_list_legacy_signal_pre_key(
List<LegacySignalPreKey> self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.length, serializer);
for (final item in self) {
sse_encode_legacy_signal_pre_key(item, serializer);
}
}
@protected
void sse_encode_list_legacy_table_migration_count(
List<LegacyTableMigrationCount> self,

View file

@ -56,12 +56,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
FutureOr<List<LegacySignalPreKey>> Function()
dco_decode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
dynamic raw,
);
@protected
FutureOr<void> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
@ -210,9 +204,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
LegacySignalPreKey dco_decode_legacy_signal_pre_key(dynamic raw);
@protected
LegacyTableMigrationCount dco_decode_legacy_table_migration_count(
dynamic raw,
@ -224,9 +215,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<FrbPqcPreKey> dco_decode_list_frb_pqc_pre_key(dynamic raw);
@protected
List<LegacySignalPreKey> dco_decode_list_legacy_signal_pre_key(dynamic raw);
@protected
List<LegacyTableMigrationCount> dco_decode_list_legacy_table_migration_count(
dynamic raw,
@ -562,11 +550,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
LegacySignalPreKey sse_decode_legacy_signal_pre_key(
SseDeserializer deserializer,
);
@protected
LegacyTableMigrationCount sse_decode_legacy_table_migration_count(
SseDeserializer deserializer,
@ -580,11 +563,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
List<LegacySignalPreKey> sse_decode_list_legacy_signal_pre_key(
SseDeserializer deserializer,
);
@protected
List<LegacyTableMigrationCount> sse_decode_list_legacy_table_migration_count(
SseDeserializer deserializer,
@ -820,13 +798,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
FutureOr<List<LegacySignalPreKey>> Function() self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
FutureOr<void> Function(PlatformInt64) self,
@ -1042,12 +1013,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_legacy_signal_pre_key(
LegacySignalPreKey self,
SseSerializer serializer,
);
@protected
void sse_encode_legacy_table_migration_count(
LegacyTableMigrationCount self,
@ -1063,12 +1028,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_legacy_signal_pre_key(
List<LegacySignalPreKey> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_legacy_table_migration_count(
List<LegacyTableMigrationCount> self,

View file

@ -58,12 +58,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
FutureOr<List<LegacySignalPreKey>> Function()
dco_decode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
dynamic raw,
);
@protected
FutureOr<void> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
@ -212,9 +206,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
LegacySignalPreKey dco_decode_legacy_signal_pre_key(dynamic raw);
@protected
LegacyTableMigrationCount dco_decode_legacy_table_migration_count(
dynamic raw,
@ -226,9 +217,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<FrbPqcPreKey> dco_decode_list_frb_pqc_pre_key(dynamic raw);
@protected
List<LegacySignalPreKey> dco_decode_list_legacy_signal_pre_key(dynamic raw);
@protected
List<LegacyTableMigrationCount> dco_decode_list_legacy_table_migration_count(
dynamic raw,
@ -564,11 +552,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
LegacySignalPreKey sse_decode_legacy_signal_pre_key(
SseDeserializer deserializer,
);
@protected
LegacyTableMigrationCount sse_decode_legacy_table_migration_count(
SseDeserializer deserializer,
@ -582,11 +565,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
List<LegacySignalPreKey> sse_decode_list_legacy_signal_pre_key(
SseDeserializer deserializer,
);
@protected
List<LegacyTableMigrationCount> sse_decode_list_legacy_table_migration_count(
SseDeserializer deserializer,
@ -822,13 +800,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
FutureOr<List<LegacySignalPreKey>> Function() self,
SseSerializer serializer,
);
@protected
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
FutureOr<void> Function(PlatformInt64) self,
@ -1044,12 +1015,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_legacy_signal_pre_key(
LegacySignalPreKey self,
SseSerializer serializer,
);
@protected
void sse_encode_legacy_table_migration_count(
LegacyTableMigrationCount self,
@ -1065,12 +1030,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_legacy_signal_pre_key(
List<LegacySignalPreKey> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_legacy_table_migration_count(
List<LegacyTableMigrationCount> self,

View file

@ -153,7 +153,6 @@ void main() async {
binding.addPostFrameCallback((_) async {
await Future.delayed(const Duration(seconds: 1));
unawaited(postStartupTasks());
unawaited(apiService.connect());
});
}

View file

@ -33,7 +33,6 @@ Future<void> initFlutterCallbacksForRust() async {
loggingGetStreamSink: LoggingCallbacks.getStreamSink,
legacySignalDecrypt: LegacySignalCallbacks.decrypt,
legacySignalEncrypt: LegacySignalCallbacks.encrypt,
legacySignalGeneratePrekeys: LegacySignalCallbacks.generatePrekeys,
apiResyncSignalSession: handleSessionResync,
apiMediaAction: _apiMediaAction,
apiVerificationProof: KeyVerificationService.handleVerificationProof,

View file

@ -3,7 +3,6 @@ import 'package:twonly/core/bridge/callbacks.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/utils/log.dart';
/// Flutter boundary for the legacy libsignal_protocol_dart implementation.
@ -62,25 +61,6 @@ abstract final class LegacySignalCallbacks {
}
}
static Future<List<LegacySignalPreKey>> generatePrekeys() async {
try {
final prekeys = await signalGetPreKeys();
return prekeys
.map(
(prekey) => LegacySignalPreKey(
id: prekey.id,
publicKey: Uint8List.fromList(
prekey.getKeyPair().publicKey.serialize(),
),
),
)
.toList(growable: false);
} catch (error) {
Log.error('Legacy Signal prekey callback failed: $error');
return const [];
}
}
static bool _isLegacyMessageType(int messageType) =>
messageType == pb.Message_Type.CIPHERTEXT.value ||
messageType == pb.Message_Type.PREKEY_BUNDLE.value;

View file

@ -5,15 +5,25 @@ import 'package:twonly/locator.dart';
class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
CustomChangeProvider() {
// The API is connected before the subscription has started so ensure that the connection state is correct
_isConnected = apiService.isConnected;
_connSub = apiService.onConnectionStateUpdated.listen(
updateConnectionState,
_isConnected = false;
unawaited(_loadConnectionState());
_connSub = apiService.events
.where((event) => event.kind == ApiEventKind.connectionStateChanged)
.listen(
(event) => updateConnectionState(
event.state == ApiConnectionState.authenticated,
),
);
}
late bool _isConnected;
late StreamSubscription<bool> _connSub;
late StreamSubscription<ApiEvent> _connSub;
bool get isConnected => _isConnected;
Future<void> _loadConnectionState() async {
final state = await RustApi.connectionState();
await updateConnectionState(state == ApiConnectionState.authenticated);
}
@override
void dispose() {
_connSub.cancel();

View file

@ -37,15 +37,22 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
onError: _updateStreamOnError,
);
_planSub = apiService.onPlanUpdated.listen(updatePlan);
_connSub = apiService.onConnectionStateUpdated.listen((_) async {
_apiSub = apiService.events.listen((event) async {
if (event.kind == ApiEventKind.planUpdated) {
updatePlan(planFromString(event.message ?? ''));
}
if (event.kind == ApiEventKind.connectionStateChanged) {
try {
if (userService.isUserCreated) {
updatePlan(planFromString(userService.currentUser.subscriptionPlan));
updatePlan(
planFromString(userService.currentUser.subscriptionPlan),
);
}
} catch (e) {
Log.error(e);
}
}
});
if (userService.isUserCreated) {
@ -63,8 +70,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
late StreamSubscription<List<PurchaseDetails>> _subscription;
final InAppPurchase iapConnection = IAPConnection.instance;
late StreamSubscription<SubscriptionPlan> _planSub;
late StreamSubscription<bool> _connSub;
late StreamSubscription<ApiEvent> _apiSub;
bool _userTriggeredBuyButton = false;
void updatePlan(SubscriptionPlan newPlan) {
@ -228,7 +234,8 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
if (currentPlan != SubscriptionPlan.Family.name &&
currentPlan != SubscriptionPlan.Pro.name) {
for (var i = 0; i < 100; i++) {
if (apiService.isAuthenticated) {
if (await RustApi.connectionState() ==
ApiConnectionState.authenticated) {
Log.info(
'current user does not have a sub: ${purchaseDetails.productID}',
);
@ -251,8 +258,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
@override
void dispose() {
_planSub.cancel();
_connSub.cancel();
_apiSub.cancel();
_subscription.cancel();
super.dispose();
}

View file

@ -1,125 +1,58 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:twonly/core/bridge/api.dart' as rust_api;
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/log.dart';
/// The ApiProvider is responsible for communicating with the server.
/// It handles errors and does automatically tries to reconnect on
/// errors or network changes.
class ApiService {
ApiService();
final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev-api.twonly.eu';
// final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu';
final String apiSecure = kReleaseMode ? 's' : 's';
String get apiEndpoint => 'http$apiSecure://$apiHost/api/';
final _planUpdateController = StreamController<SubscriptionPlan>.broadcast();
Stream<SubscriptionPlan> get onPlanUpdated => _planUpdateController.stream;
final _connectionStateController = StreamController<bool>.broadcast();
Stream<bool> get onConnectionStateUpdated =>
_connectionStateController.stream;
final _appOutdatedController = StreamController<void>.broadcast();
Stream<void> get onAppOutdated => _appOutdatedController.stream;
final _newDeviceRegisteredController = StreamController<void>.broadcast();
Stream<void> get onNewDeviceRegistered =>
_newDeviceRegisteredController.stream;
bool appIsOutdated = false;
bool isAuthenticated = false;
bool isConnected = false;
// ignore: cancel_subscriptions
ApiService() {
events = rust_api.RustApi.events().asBroadcastStream();
_apiEventSubscription = events.listen(
_handleApiEvent,
onError: (Object error, StackTrace stackTrace) {
Log.error(
'Rust API event stream failed',
error: error,
stackTrace: stackTrace,
);
},
);
}
late final Stream<ApiEvent> events;
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
late final StreamSubscription<ApiEvent> _apiEventSubscription;
Future<void> _handleApiEvent(ApiEvent event) async {
if (event.kind == ApiEventKind.authenticated) {
await onAuthenticated();
}
}
// Function is called after the user is authenticated at the server
Future<void> onAuthenticated() async {
await FcmNotificationService.initFCMAfterAuthenticated();
_connectionStateController.add(true);
if (AppState.isInBackgroundTask) {
await RustApi.retransmitAllMessages();
await reuploadMediaFiles();
await tryDownloadAllMediaFiles();
} else if (!AppState.isAppInBackground) {
unawaited(RustApi.retransmitAllMessages());
unawaited(tryDownloadAllMediaFiles());
unawaited(reuploadMediaFiles());
twonlyDB.markUpdated();
unawaited(syncFlameCounters());
unawaited(SignalIdentityService.onAuthenticated());
resetResyncedUsers();
// resetUserDiscoveryRequestUpdates();
unawaited(fetchGroupStatesForUnjoinedGroups());
unawaited(fetchMissingGroupPublicKey());
unawaited(rust_api.RustApi.checkForDeletedUsernames());
unawaited(RustApi.performPasswordlessRecoveryHeartbeat());
unawaited(UserDiscoveryService.checkForNewAnnouncedUsers());
memoriesCloudService.init();
}
}
Future<bool> connect() async {
try {
await rust_api.RustApi.connect();
for (var attempt = 0; attempt < 100; attempt++) {
final state = await rust_api.RustApi.connectionState();
isConnected =
state == ApiConnectionState.connected ||
state == ApiConnectionState.authenticated;
isAuthenticated = state == ApiConnectionState.authenticated;
if (isAuthenticated ||
(!userService.isUserCreated &&
state == ApiConnectionState.connected)) {
return true;
}
if (state == ApiConnectionState.permanentlyRejected ||
state == ApiConnectionState.suspended) {
return false;
}
await Future<void>.delayed(const Duration(milliseconds: 100));
}
return false;
} catch (error) {
isConnected = false;
isAuthenticated = false;
Log.error('Rust API connection failed', error: error);
return false;
}
}
Future<void> close(VoidCallback? callback) async {
await rust_api.RustApi.close();
isConnected = false;
isAuthenticated = false;
_connectionStateController.add(false);
callback?.call();
}
Future<void> authenticate() async {
await rust_api.RustApi.reloadConfiguration();
await connect();
}
Future<void> listenToNetworkChanges() async {
if (_connectivitySubscription != null) {
return;
@ -127,10 +60,14 @@ class ApiService {
_connectivitySubscription = Connectivity().onConnectivityChanged.listen((
result,
) async {
if (!result.contains(ConnectivityResult.none)) {
await connect();
}
// Received changes in available connectivity types!
await rust_api.RustApi.setNetworkAvailable(
available: !result.contains(ConnectivityResult.none),
);
});
}
Future<void> dispose() async {
await _connectivitySubscription?.cancel();
await _apiEventSubscription.cancel();
}
}

View file

@ -779,8 +779,7 @@ Future<void> _uploadUploadRequest(MediaFileService media) async {
return;
}
final apiUrl =
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
final apiUrl = '${RustApi.apiBaseUrl(protocol: 'https')}upload';
Log.info('Starting upload from ${media.mediaFile.mediaId}');

View file

@ -127,17 +127,20 @@ Future<bool> backgroundFetch({
final stopwatch = Stopwatch()..start();
// Issue: Because the background isolate can be reused across multiple periodic tasks,
// the API connection state might be stale or disconnected from a previous run.
// Explicitly close it here to ensure a clean slate before connecting.
await apiService.close(null);
if (!await apiService.connect()) {
Log.info('Could not connect to the api. Returning early.');
return false;
var authenticated = false;
for (var attempt = 0; attempt < 100; attempt++) {
final state = await RustApi.connectionState();
if (state == ApiConnectionState.authenticated) {
authenticated = true;
break;
}
if (!apiService.isAuthenticated) {
if (state == ApiConnectionState.permanentlyRejected ||
state == ApiConnectionState.suspended) {
break;
}
await Future<void>.delayed(const Duration(milliseconds: 100));
}
if (!authenticated) {
Log.info('Api is not authenticated. Returning early.');
return false;
}
@ -164,7 +167,7 @@ Future<bool> backgroundFetch({
await Future.delayed(const Duration(milliseconds: 2000));
}
} finally {
await apiService.close(() {});
await RustApi.close();
stopwatch.stop();
}

View file

@ -23,10 +23,10 @@ class BackupService {
static final Mutex _protected = Mutex();
static String _getIdentityBackupUrl(String backupId) =>
'${apiService.apiEndpoint}/backup/identity/$backupId';
'${RustApi.apiBaseUrl(protocol: 'https')}backup/identity/$backupId';
static String _getArchiveBackupUrl(String backupDownloadToken, int? userId) =>
'${apiService.apiEndpoint}/backup/archive/${userId == null ? '' : '${userId.toRadixString(16).padLeft(16, '0').toUpperCase()}/'}$backupDownloadToken';
'${RustApi.apiBaseUrl(protocol: 'https')}backup/archive/${userId == null ? '' : '${userId.toRadixString(16).padLeft(16, '0').toUpperCase()}/'}$backupDownloadToken';
static final _backupUpdateController = StreamController<void>.broadcast();
static Stream<void> get onBackupUpdated => _backupUpdateController.stream;

View file

@ -97,7 +97,8 @@ class FcmNotificationService {
..updateFcmToken = true
..fcmToken = fcmToken;
});
if (apiService.isAuthenticated) {
if (await RustApi.connectionState() ==
ApiConnectionState.authenticated) {
if (await _uploadFcmToken(fcmToken)) {
await UserService.update((u) {
u.updateFcmToken = false;
@ -114,7 +115,8 @@ class FcmNotificationService {
..updateFcmToken = true
..fcmToken = fcmToken;
});
if (apiService.isAuthenticated) {
if (await RustApi.connectionState() ==
ApiConnectionState.authenticated) {
if (await _uploadFcmToken(fcmToken)) {
await UserService.update((u) {
u.updateFcmToken = false;
@ -161,7 +163,10 @@ class FcmNotificationService {
// This is just a workarround until the new Rust decryption is enrolled fully.
final pushDataString = message.data['push_data'] as String?;
if (pushDataString != null) {
if (apiService.isConnected) {
final apiState = await RustApi.connectionState();
if (apiState == ApiConnectionState.connected ||
apiState == ApiConnectionState.authenticating ||
apiState == ApiConnectionState.authenticated) {
Log.info('Got FCM message, but API is connected...');
} else {
Log.info('Trying to connect to the API in the background.');

View file

@ -1,99 +1,13 @@
import 'dart:typed_data';
import 'package:clock/clock.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/signal/signal_signed_pre_key_store.dart';
import 'package:twonly/src/model/json/signal_identity.model.dart';
import 'package:twonly/src/services/signal/consts.signal.dart';
import 'package:twonly/src/services/signal/protocol_state.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
class SignalIdentityService {
static Future<void> onAuthenticated() async {
final now = clock.now();
final fortyEightHoursAgo = now.subtract(const Duration(hours: 48));
final oneWeekAgo = now.subtract(const Duration(days: 7));
if (userService.currentUser.signalLastSignedPreKeyUpdated == null ||
!userService.currentUser.signalLastSignedPreKeyUpdated!.isAfter(
fortyEightHoursAgo,
)) {
final signedPreKey = await _getNewSignalSignedPreKey();
if (signedPreKey == null) {
Log.error('could not generate a new signed pre key!');
} else {
await UserService.update((user) {
user.signalLastSignedPreKeyUpdated = now;
});
final res = await rustApiResult(
RustApi.updateSignedPreKey(
id: signedPreKey.id,
key: signedPreKey.getKeyPair().publicKey.serialize(),
signature: signedPreKey.signature,
),
);
if (res.isError) {
Log.error('could not update the signed pre key: ${res.error}');
await UserService.update((user) {
user.signalLastSignedPreKeyUpdated = null;
});
} else {
Log.info('updated signed pre key');
}
}
}
if (userService.currentUser.signalLastPqcPreKeysUploaded == null ||
!userService.currentUser.signalLastPqcPreKeysUploaded!.isAfter(
oneWeekAgo,
)) {
final bundle = await RustSignal.generateBundle();
final pqcRes = await rustApiResult(
RustApi.uploadPqcPreKeys(
eccSignedPrekeyId: bundle.signedPreKeyId,
eccSignedPrekey: bundle.signedPreKeyPublic,
eccSignedPrekeySignature: bundle.signedPreKeySignature,
kyberSignedPrekeyId: bundle.kyberPreKeyId,
kyberSignedPrekey: bundle.kyberPreKeyPublic,
kyberSignedPrekeySignature: bundle.kyberPreKeySignature,
prekeys: const [],
),
);
if (pqcRes.isError) {
Log.warn('could not update the pqc signed pre key: ${pqcRes.error}');
} else {
Log.info('updated pqc signed pre key');
await UserService.update((user) {
user.signalLastPqcPreKeysUploaded = now;
});
}
}
}
}
Future<List<PreKeyRecord>> signalGetPreKeys() async {
return lockingSignalProtocol.protect(() async {
final start = userService.currentUser.currentPreKeyIndexStart;
await UserService.update((u) {
u.currentPreKeyIndexStart = (u.currentPreKeyIndexStart + 200) % maxValue;
});
final preKeys = generatePreKeys(start, 200);
final signalStore = await getSignalStore();
if (signalStore == null) return [];
for (final p in preKeys) {
await signalStore.preKeyStore.storePreKey(p.id, p);
}
return preKeys;
});
}
Future<SignalIdentity?> getSignalIdentity() async {
try {
final identity = await RustKeyManager.getSignalIdentity();
@ -156,30 +70,3 @@ Future<void> createIfNotExistsSignalIdentity() async {
signedPreKeyStore: const {},
);
}
Future<SignedPreKeyRecord?> _getNewSignalSignedPreKey() async {
return lockingSignalProtocol.protect(() async {
var identityKeyPair = await getSignalIdentityKeyPair();
final signalStore = await getSignalStore();
if (identityKeyPair == null || signalStore == null) {
return null;
}
final signedPreKeyId =
userService.currentUser.currentSignedPreKeyIndexStart;
await UserService.update((user) {
user.currentSignedPreKeyIndexStart += 1;
});
final signedPreKey = generateSignedPreKey(
identityKeyPair,
signedPreKeyId,
);
identityKeyPair = null;
await signalStore.storeSignedPreKey(signedPreKeyId, signedPreKey);
return signedPreKey;
});
}

View file

@ -19,33 +19,25 @@ class _AppOutdatedCompState extends State<AppOutdatedComp> {
bool appIsOutdated = false;
bool newDeviceRegistered = false;
late StreamSubscription<void> _subOutdated;
late StreamSubscription<void> _subNewDevice;
late StreamSubscription<ApiEvent> _apiEventSubscription;
@override
void dispose() {
_subOutdated.cancel();
_subNewDevice.cancel();
_apiEventSubscription.cancel();
super.dispose();
}
@override
void initState() {
super.initState();
_subOutdated = apiService.onAppOutdated.listen((_) async {
if (mounted) {
_apiEventSubscription = apiService.events.listen((event) async {
if (!mounted) return;
if (event.kind == ApiEventKind.appOutdated ||
event.kind == ApiEventKind.newDeviceRegistered) {
await context.read<CustomChangeProvider>().updateConnectionState(false);
setState(() {
appIsOutdated = true;
});
}
});
_subNewDevice = apiService.onNewDeviceRegistered.listen((_) async {
if (mounted) {
await context.read<CustomChangeProvider>().updateConnectionState(false);
setState(() {
newDeviceRegistered = true;
appIsOutdated = event.kind == ApiEventKind.appOutdated;
newDeviceRegistered = event.kind == ApiEventKind.newDeviceRegistered;
});
}
});

View file

@ -361,8 +361,8 @@ class _ChatListViewState extends State<ChatListView>
),
body: RefreshIndicator(
onRefresh: () async {
await apiService.close(() {});
await apiService.connect();
await RustApi.close();
await RustApi.connect();
await Future.delayed(const Duration(seconds: 1));
},
child: Column(

View file

@ -176,7 +176,7 @@ class _RegisterViewState extends State<RegisterView> {
unawaited(FcmNotificationService.initAfterUserLoaded());
await apiService.authenticate();
await RustApi.reloadConfiguration();
widget.callbackOnSuccess();
} catch (e, stack) {
Log.error('Error creating new user', error: e, stackTrace: stack);

View file

@ -68,8 +68,7 @@ class _ContactUsState extends State<ContactUsView> {
final uploadRequestBytes = uploadRequest.writeToBuffer();
final apiUrl =
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
final apiUrl = '${RustApi.apiBaseUrl(protocol: 'https')}upload';
final requestMultipart = http.MultipartRequest('POST', Uri.parse(apiUrl));

View file

@ -29,15 +29,6 @@ pub(crate) async fn handle_server_message(
kind: server_to_client::v0::Kind,
) -> Result<client_to_server::Response> {
let ok = match kind {
// These booleans are presence-only request markers. Their value is not part of
// the protocol; the Dart client likewise checks hasRequestNewPreKeys() only.
Kind::RequestNewPreKeys(_) => match handle_request_new_prekeys(ctx).await {
Ok(response) => response,
Err(error) => {
tracing::error!("failed to generate requested prekeys: {error}");
ok::Ok::None(true)
}
},
Kind::RequestNewPqcPreKeys(_) => match handle_request_new_pqc_prekeys(ctx).await {
Ok(response) => response,
Err(error) => {
@ -107,40 +98,6 @@ pub(crate) async fn handle_sealed_message(ctx: &Arc<Context>, bytes: Vec<u8>) ->
handle_decoded_server_message(ctx, payload.from_user_id, message).await
}
pub(crate) async fn handle_request_new_prekeys(
ctx: &Arc<Context>,
) -> Result<client_to_server::response::ok::Ok> {
let prekeys = match get_callbacks() {
Ok(callbacks) => (callbacks.legacy_signal.generate_prekeys)()
.await
.into_iter()
.map(|key| client_to_server::response::PreKey {
id: key.id,
prekey: key.public_key,
})
.collect(),
Err(TwonlyError::MissingCallbackInitialization) => {
let engine = ctx.get_signal_engine().lock().await;
engine
.as_ref()
.ok_or(TwonlyError::SignalIdentityNotFound)?
.generate_prekeys(200)
.await?
.into_iter()
.map(|(id, public_key)| client_to_server::response::PreKey {
id: i64::from(id),
prekey: public_key,
})
.collect()
}
Err(error) => return Err(error),
};
Ok(client_to_server::response::ok::Ok::Prekeys(
client_to_server::response::Prekeys { prekeys },
))
}
pub(crate) async fn handle_request_new_pqc_prekeys(
ctx: &Arc<Context>,
) -> Result<client_to_server::response::ok::Ok> {

View file

@ -38,7 +38,7 @@ impl ApiRuntime {
.ok_or(TwonlyError::Initialization)?;
*slot.write().await = replacement;
current.close().await;
Ok(())
Self::client(ctx).await?.connect().await
}
pub async fn connect(ctx: &Arc<Context>) -> Result<()> {

View file

@ -19,6 +19,7 @@ pub(super) type PendingRequests = Arc<Mutex<HashMap<u64, oneshot::Sender<Vec<u8>
pub(crate) static API_EVENTS: LazyLock<broadcast::Sender<ApiEvent>> =
LazyLock::new(|| broadcast::channel(256).0);
pub(crate) static API_PERMANENTLY_REJECTED: AtomicBool = AtomicBool::new(false);
pub(crate) struct ApiClient {
pub(crate) context: Weak<Context>,
@ -65,6 +66,13 @@ impl ApiClient {
}
pub async fn connect(self: &Arc<Self>) -> Result<()> {
if API_PERMANENTLY_REJECTED.load(Ordering::Acquire) {
self.set_state(ApiConnectionState::PermanentlyRejected)
.await;
return Err(crate::error::TwonlyError::Generic(
"API connection was permanently rejected for this process".into(),
));
}
self.deliberately_closed.store(false, Ordering::Release);
let mut client_guard = self.ws_client.lock().await;
@ -139,8 +147,12 @@ impl ApiClient {
}
Ok(ConnectionEvent::Disconnected { .. }) => {
self_clone.is_authenticated.store(false, Ordering::Release);
if API_PERMANENTLY_REJECTED.load(Ordering::Acquire) {
self_clone.set_state(ApiConnectionState::PermanentlyRejected).await;
} else {
self_clone.set_state(ApiConnectionState::Stopped).await;
}
}
Ok(ConnectionEvent::Connecting { .. }) => {
self_clone.set_state(ApiConnectionState::Connecting).await;
}

View file

@ -3,13 +3,16 @@
*
*/
use crate::api::messages::incoming::client2client::messages;
use crate::api::messages::incoming::client2client::{messages, recovery};
use crate::api::proto::server_to_client;
use crate::api::runtime::ApiRuntime;
use crate::api::Server;
use crate::bridge::api::ServerResult;
use crate::context::Context;
use crate::error::{Result, TwonlyError};
use crate::services::groups::GroupService;
use crate::services::mediafiles::MediaFileService;
use prost::Message as ProstMessage;
use std::future::Future;
use std::pin::Pin;
@ -37,9 +40,6 @@ pub(crate) fn response_error_code(bytes: &[u8]) -> Result<Option<i32>> {
}
pub(crate) fn schedule_post_authentication(ctx: &Arc<Context>, in_background: bool) {
if in_background {
return;
}
let ctx = ctx.clone();
tokio::spawn(async move {
// Wait a bit to let other initial state settle
@ -50,12 +50,50 @@ pub(crate) fn schedule_post_authentication(ctx: &Arc<Context>, in_background: bo
if let Err(error) = replay.await {
tracing::warn!("failed to replay API outbox: {error}");
}
if let Err(error) = ApiRuntime::replay_legacy_raw_outbox(&ctx).await {
tracing::warn!("failed to replay legacy raw-byte outbox: {error}");
}
if let Err(error) = messages::retransmit_queued_receipts(&ctx).await {
tracing::warn!("failed to retransmit queued receipts: {error}");
}
if let Err(error) = MediaFileService::new(&ctx).download_pending().await {
tracing::warn!("failed to download pending media: {error}");
}
if in_background {
return;
}
if let Err(error) = GroupService::new(&ctx).on_connected().await {
tracing::warn!("group post-connection maintenance failed: {error}");
}
if let Err(error) = Server::check_for_deleted_usernames(&ctx).await {
tracing::warn!("deleted-username refresh failed: {error}");
}
if let Err(error) = recovery::perform_heartbeat(&ctx).await {
tracing::warn!("passwordless recovery heartbeat failed: {error}");
}
if let Err(error) = ctx
.get_user_discovery()
.get()
.await
.on_connected(&ctx)
.await
{
tracing::warn!("user-discovery post-connection refresh failed: {error}");
}
let signal_engine = ctx.get_signal_engine().lock().await;
if let Some(engine) = signal_engine.as_ref() {
if let Err(error) = engine.on_connected(&ctx).await {
tracing::warn!("Signal key maintenance failed: {error}");
}
}
});
}

View file

@ -3,7 +3,7 @@
*
*/
use super::client::ApiClient;
use super::client::{ApiClient, API_PERMANENTLY_REJECTED};
use crate::api::messages::incoming::handle_server_message;
use crate::api::proto::{client_to_server, server_to_client};
use crate::bridge::api::{ApiConnectionState, ApiEvent, ApiEventKind};
@ -203,6 +203,7 @@ impl ApiClient {
{
self.set_state(ApiConnectionState::PermanentlyRejected)
.await;
API_PERMANENTLY_REJECTED.store(true, Ordering::Release);
self.deliberately_closed.store(true, Ordering::Release);
let kind = if code == ErrorCode::AppVersionOutdated as i32 {
ApiEventKind::AppOutdated
@ -214,8 +215,10 @@ impl ApiClient {
state: Some(ApiConnectionState::PermanentlyRejected),
message: None,
});
if let Some(_) = self.ws_client.lock().await.take() {
// Drop client to disconnect
if let Some(client) = self.ws_client.lock().await.take() {
if let Err(error) = client.shutdown_graceful(Duration::from_secs(5)).await {
tracing::warn!(%error, "permanently rejected WebSocket did not shut down cleanly");
}
}
}
if code == ErrorCode::UserIdNotFound as i32 {

View file

@ -35,12 +35,6 @@ pub struct LegacySignalEncryptResult {
pub message_type: i32,
}
#[derive(Clone, Debug)]
pub struct LegacySignalPreKey {
pub id: i64,
pub public_key: Vec<u8>,
}
// This will also generate the function init_flutter_callbacks which MUST be called from Flutter to initialize the callbacks
callback_generator! {
FlutterCallbacks {
@ -49,8 +43,7 @@ callback_generator! {
},
LegacySignal legacy_signal {
decrypt: (i64, Vec<u8>, i32) => LegacySignalDecryptResult,
encrypt: (i64, Vec<u8>) => Option<LegacySignalEncryptResult>,
generate_prekeys: () => Vec<LegacySignalPreKey>
encrypt: (i64, Vec<u8>) => Option<LegacySignalEncryptResult>
},
Api api {
resync_signal_session: (i64) => (),

View file

@ -15,6 +15,32 @@ const MAX_FUTURE_TIMESTAMP_SKEW_SECONDS: i64 = 10 * 60;
pub struct Group;
impl Group {
pub async fn flame_sync_candidates(pool: &sqlx::Pool<Sqlite>) -> Result<Vec<FlameSyncGroup>> {
Ok(sqlx::query_as!(
FlameSyncGroup,
r#"SELECT group_id, total_media_counter, last_flame_counter_change,
last_flame_sync, flame_counter
FROM groups WHERE last_flame_counter_change IS NOT NULL"#
)
.fetch_all(pool)
.await?)
}
pub async fn set_last_flame_sync(
pool: &sqlx::Pool<Sqlite>,
group_id: &str,
timestamp: i64,
) -> Result<()> {
sqlx::query!(
"UPDATE groups SET last_flame_sync = ? WHERE group_id = ?",
timestamp,
group_id,
)
.execute(pool)
.await?;
Ok(())
}
pub async fn ensure_exists(tr: &mut Transaction<'_, Sqlite>, group_id: &str) -> Result<()> {
let exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM groups WHERE group_id = ?)",
@ -149,6 +175,14 @@ impl Group {
}
}
pub struct FlameSyncGroup {
pub group_id: String,
pub total_media_counter: i64,
pub last_flame_counter_change: Option<i64>,
pub last_flame_sync: Option<i64>,
pub flame_counter: i64,
}
#[derive(bon::Builder)]
pub struct InsertGroup {
group_id: String,
@ -462,8 +496,5 @@ impl GetMissingGroupPublicKeys {
}
fn current_unix_timestamp() -> Result<i64> {
Ok(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|error| TwonlyError::Generic(error.to_string()))?
.as_secs() as i64)
Ok(crate::utils::current_time().timestamp())
}

View file

@ -334,7 +334,6 @@ fn wire__crate__bridge__callbacks__init_flutter_callbacks_impl(
let api_logging_get_stream_sink = decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_legacy_signal_decrypt = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_i_32_Output_legacy_signal_decrypt_result_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_legacy_signal_encrypt = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_legacy_signal_generate_prekeys = decode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_resync_signal_session = decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_media_action = decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_verification_proof = decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
@ -342,7 +341,7 @@ let api_api_create_push_avatars = decode_DartFn_Inputs_i_64_Output_unit_AnyhowEx
let api_api_media_received = decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));
let api_api_user_config_changed = decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(<flutter_rust_bridge::DartOpaque>::sse_decode(&mut deserializer));deserializer.end(); move |context| {
transform_result_sse::<_, ()>((move || {
let output_ok = Result::<_,()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks(api_callback_id, api_logging_get_stream_sink, api_legacy_signal_decrypt, api_legacy_signal_encrypt, api_legacy_signal_generate_prekeys, api_api_resync_signal_session, api_api_media_action, api_api_verification_proof, api_api_create_push_avatars, api_api_media_received, api_api_user_config_changed); })?; Ok(output_ok)
let output_ok = Result::<_,()>::Ok({ crate::bridge::callbacks::init_flutter_callbacks(api_callback_id, api_logging_get_stream_sink, api_legacy_signal_decrypt, api_legacy_signal_encrypt, api_api_resync_signal_session, api_api_media_action, api_api_verification_proof, api_api_create_push_avatars, api_api_media_received, api_api_user_config_changed); })?; Ok(output_ok)
})())
} })
}
@ -4202,40 +4201,6 @@ fn decode_DartFn_Inputs__Output_StreamSink_String_Sse_AnyhowException(
flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body(dart_opaque.clone()))
}
}
fn decode_DartFn_Inputs__Output_list_legacy_signal_pre_key_AnyhowException(
dart_opaque: flutter_rust_bridge::DartOpaque,
) -> impl Fn() -> flutter_rust_bridge::DartFnFuture<Vec<crate::bridge::callbacks::LegacySignalPreKey>>
{
use flutter_rust_bridge::IntoDart;
async fn body(
dart_opaque: flutter_rust_bridge::DartOpaque,
) -> Vec<crate::bridge::callbacks::LegacySignalPreKey> {
let args = vec![];
let message = FLUTTER_RUST_BRIDGE_HANDLER
.dart_fn_invoke(dart_opaque, args)
.await;
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let action = deserializer.cursor.read_u8().unwrap();
let ans = match action {
0 => std::result::Result::Ok(
<Vec<crate::bridge::callbacks::LegacySignalPreKey>>::sse_decode(&mut deserializer),
),
1 => std::result::Result::Err(
<flutter_rust_bridge::for_generated::anyhow::Error>::sse_decode(&mut deserializer),
),
_ => unreachable!(),
};
deserializer.end();
let ans = ans.expect("Dart throws exception but Rust side assume it is not failable");
ans
}
move || {
flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body(dart_opaque.clone()))
}
}
fn decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
dart_opaque: flutter_rust_bridge::DartOpaque,
) -> impl Fn(i64) -> flutter_rust_bridge::DartFnFuture<()> {
@ -4728,18 +4693,6 @@ impl SseDecode for crate::bridge::callbacks::LegacySignalEncryptResult {
}
}
impl SseDecode for crate::bridge::callbacks::LegacySignalPreKey {
// 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_id = <i64>::sse_decode(deserializer);
let mut var_publicKey = <Vec<u8>>::sse_decode(deserializer);
return crate::bridge::callbacks::LegacySignalPreKey {
id: var_id,
public_key: var_publicKey,
};
}
}
impl SseDecode for crate::bridge::wrapper::app_database::LegacyTableMigrationCount {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -4778,20 +4731,6 @@ impl SseDecode for Vec<crate::signal::engine::FrbPqcPreKey> {
}
}
impl SseDecode for Vec<crate::bridge::callbacks::LegacySignalPreKey> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut len_ = <i32>::sse_decode(deserializer);
let mut ans_ = Vec::with_capacity(len_ as usize);
for idx_ in 0..len_ {
ans_.push(<crate::bridge::callbacks::LegacySignalPreKey>::sse_decode(
deserializer,
));
}
return ans_;
}
}
impl SseDecode for Vec<crate::bridge::wrapper::app_database::LegacyTableMigrationCount> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@ -5913,27 +5852,6 @@ impl flutter_rust_bridge::IntoIntoDart<crate::bridge::callbacks::LegacySignalEnc
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::bridge::callbacks::LegacySignalPreKey {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.id.into_into_dart().into_dart(),
self.public_key.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::bridge::callbacks::LegacySignalPreKey
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::bridge::callbacks::LegacySignalPreKey>
for crate::bridge::callbacks::LegacySignalPreKey
{
fn into_into_dart(self) -> crate::bridge::callbacks::LegacySignalPreKey {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart
for crate::bridge::wrapper::app_database::LegacyTableMigrationCount
{
@ -6651,14 +6569,6 @@ impl SseEncode for crate::bridge::callbacks::LegacySignalEncryptResult {
}
}
impl SseEncode for crate::bridge::callbacks::LegacySignalPreKey {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i64>::sse_encode(self.id, serializer);
<Vec<u8>>::sse_encode(self.public_key, serializer);
}
}
impl SseEncode for crate::bridge::wrapper::app_database::LegacyTableMigrationCount {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@ -6687,16 +6597,6 @@ impl SseEncode for Vec<crate::signal::engine::FrbPqcPreKey> {
}
}
impl SseEncode for Vec<crate::bridge::callbacks::LegacySignalPreKey> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode(self.len() as _, serializer);
for item in self {
<crate::bridge::callbacks::LegacySignalPreKey>::sse_encode(item, serializer);
}
}
}
impl SseEncode for Vec<crate::bridge::wrapper::app_database::LegacyTableMigrationCount> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {

View file

@ -19,7 +19,7 @@ use crate::api::server::Server;
use crate::bridge::api::ServerResult;
use crate::context::Context;
use crate::database::app::tables::{
Contact, GetGroupPublicKey, GetMissingGroupPublicKeys, GetUnjoinedGroups, InsertGroup,
Contact, GetGroupPublicKey, GetMissingGroupPublicKeys, GetUnjoinedGroups, Group, InsertGroup,
InsertGroupHistory, InsertGroupMember, UpdateContact, UpdateGroup, UpdateGroupMemberState,
};
use crate::error::{Result, TwonlyError};
@ -38,6 +38,59 @@ pub struct GroupService {
}
impl GroupService {
pub async fn on_connected(&self) -> Result<()> {
self.fetch_group_states_for_unjoined_groups().await?;
self.fetch_missing_group_public_keys().await?;
self.sync_flame_counters().await
}
async fn sync_flame_counters(&self) -> Result<()> {
let db = self.ctx.get_app_database().await;
let groups = Group::flame_sync_candidates(&db.pool).await?;
let Some(best_friend) = groups.iter().max_by_key(|group| group.total_media_counter) else {
return Ok(());
};
let best_friend_id = best_friend.group_id.clone();
let now = current_time().timestamp();
let start_today = now - now.rem_euclid(86_400);
for group in groups {
let Some(changed) = group.last_flame_counter_change else {
continue;
};
if changed < start_today
|| group
.last_flame_sync
.is_some_and(|sync| sync >= start_today)
{
continue;
}
if group.flame_counter <= 2 && group.group_id != best_friend_id {
continue;
}
MessageService::new(&self.ctx)
.send_to_group(
group.group_id.clone(),
EncryptedContent {
flame_sync: Some(encrypted_content::FlameSync {
flame_counter: group.flame_counter,
last_flame_counter_change: changed * 1000,
best_friend: group.group_id == best_friend_id,
force_update: false,
}),
..Default::default()
}
.encode_to_vec(),
None,
false,
)
.await?;
Group::set_last_flame_sync(&db.pool, &group.group_id, now).await?;
}
db.notify_committed(["groups"]);
Ok(())
}
pub fn new(ctx: &Arc<Context>) -> Self {
Self { ctx: ctx.clone() }
}

View file

@ -3,7 +3,10 @@
*
*/
use crate::api::server::Server;
use crate::error::{Result, TwonlyError};
use crate::user_config::UserConfig;
use chrono::{Duration, Utc};
use libsignal_protocol::{
message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, GenericSignedPreKey,
IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle,
@ -17,6 +20,7 @@ use tokio::sync::Mutex;
use crate::bridge::get_twonly_flutter;
use crate::signal::assert_send::AssertSendFutureExt;
use crate::signal::store::DbSignalProtocolStore;
use crate::utils::current_time;
use rand::SeedableRng;
pub struct RustSignalEngine {
@ -47,6 +51,53 @@ pub struct FrbPqcPreKey {
}
impl RustSignalEngine {
pub async fn on_connected(&self, ctx: &Arc<crate::context::Context>) -> Result<()> {
let base = UserConfig::load_required_from(ctx)?;
let now = current_time().with_timezone(&Utc);
let refresh_signed = base
.signal_last_signed_pre_key_updated
.is_none_or(|last| last < now - Duration::hours(48));
let refresh_pqc = base
.signal_last_pqc_pre_keys_uploaded
.is_none_or(|last| last < now - Duration::days(7));
if !refresh_signed && !refresh_pqc {
return Ok(());
}
let bundle = self.generate_bundle().await?;
if refresh_signed {
Server::update_signed_pre_key(
ctx,
i64::from(bundle.signed_pre_key_id),
bundle.signed_pre_key_public.clone(),
bundle.signed_pre_key_signature.clone(),
)
.await?;
UserConfig::update(ctx, |config| {
config.signal_last_signed_pre_key_updated = Some(now);
})?;
}
if refresh_pqc {
Server::upload_pqc_pre_keys(
ctx,
i64::from(bundle.signed_pre_key_id),
bundle.signed_pre_key_public,
bundle.signed_pre_key_signature,
i64::from(bundle.kyber_pre_key_id),
bundle.kyber_pre_key_public,
bundle.kyber_pre_key_signature,
Vec::new(),
)
.await?;
UserConfig::update(ctx, |config| {
config.signal_last_pqc_pre_keys_uploaded = Some(now);
})?;
}
Ok(())
}
pub async fn new(local_name: String) -> Result<Self> {
let twonly = get_twonly_flutter()?;
let pool = twonly.rust_db.read().await.pool.clone();

View file

@ -372,6 +372,20 @@ impl UserConfig {
Self::save_json_unlocked(context, json)
}
/// Atomically updates the latest persisted configuration with a typed Rust
/// mutation. Unlike `update_json`, this does not need a caller snapshot:
/// loading, mutation, and saving all happen while holding the write lock.
pub(crate) fn update(context: &Context, mutate: impl FnOnce(&mut Self)) -> Result<()> {
let _guard = config_lock()
.write()
.map_err(|_| twonly_error!("user configuration lock was poisoned"))?;
let mut config = Self::load_from_unlocked(context)?
.ok_or_else(|| twonly_error!("user configuration is unavailable"))?;
mutate(&mut config);
Self::save_unlocked(context, &config)?;
Ok(())
}
/// Applies only fields changed relative to the caller's original snapshot.
/// Concurrent updates from Flutter isolates or Rust therefore do not
/// overwrite unrelated fields with stale values.
@ -399,6 +413,10 @@ impl UserConfig {
let config: Self = serde_json::from_str(json).map_err(|error| {
TwonlyError::Generic(format!("invalid user configuration update: {error}"))
})?;
Self::save_unlocked(context, &config)
}
fn save_unlocked(context: &Context, config: &Self) -> Result<String> {
let normalized = serde_json::to_string(&config)?;
let path = Self::path(context);
let parent = path

View file

@ -4,6 +4,9 @@
*/
use std::collections::HashSet;
use crate::api::server::Server;
use crate::bridge::api::ServerResult;
use crate::context::Context;
use std::path::PathBuf;
use std::sync::Arc;
use blahaj::{Share, Sharks};
@ -70,6 +73,55 @@ pub struct UserDiscovery {
}
impl UserDiscovery {
/// Refreshes server-owned data for announcements after the API connection
/// has authenticated. Cryptographic discovery state remains owned here;
/// the API runtime only invokes this lifecycle hook.
pub async fn on_connected(&self, ctx: &Arc<Context>) -> Result<()> {
let database = ctx.get_app_database().await;
let announcements = sqlx::query!(
r#"SELECT announced_user_id, announced_public_key
FROM user_discovery_announced_users WHERE username IS NULL"#
)
.fetch_all(&database.pool)
.await?;
for announcement in announcements {
let user = match Server::get_user_by_id(ctx, announcement.announced_user_id).await? {
ServerResult::Ok(user) => user,
ServerResult::ErrorCode(code) => {
tracing::warn!(
user_id = announcement.announced_user_id,
code,
"could not refresh announced user"
);
continue;
}
};
if user.public_identity_key.as_deref()
!= Some(announcement.announced_public_key.as_slice())
{
tracing::error!(
user_id = announcement.announced_user_id,
"server returned a different identity key for announced user"
);
continue;
}
let Some(username) = user.username else {
continue;
};
let username = String::from_utf8(username)?;
sqlx::query!(
"UPDATE user_discovery_announced_users SET username = ? WHERE announced_user_id = ?",
username,
announcement.announced_user_id,
)
.execute(&database.pool)
.await?;
}
database.notify_committed(["user_discovery_announced_users"]);
Ok(())
}
pub fn new(
data_dir: &str,
key_manager: Arc<Mutex<KeyManager>>,