moving userconfig ownership to rust

This commit is contained in:
otsmr 2026-08-28 14:41:25 +02:00
parent 6f16ee85d7
commit bd65268dd2
143 changed files with 7556 additions and 9763 deletions

View file

@ -220,7 +220,11 @@ class _AppMainWidgetState extends State<AppMainWidget> {
} else { } else {
// This means the user is in the onboarding screen, so start with the Proof of Work. // This means the user is in the onboarding screen, so start with the Proof of Work.
final (proof, disabled) = await apiService.getProofOfWork(); final proofResult = await rustApiResult(RustApi.getProofOfWork());
final proof = proofResult.value == null
? null
: decodeProofOfWork(proofResult.value!);
final disabled = proofResult.error == ErrorCode.RegistrationDisabled;
if (proof != null) { if (proof != null) {
Log.info('Starting with proof of work calculation.'); Log.info('Starting with proof of work calculation.');
_proofOfWork = ( _proofOfWork = (

View file

@ -0,0 +1,32 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../../../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class PasswordLessRecovery {
final Uint8List? recoverySecretShare;
final bool delete;
final PlatformInt64 threshold;
const PasswordLessRecovery({
this.recoverySecretShare,
required this.delete,
required this.threshold,
});
@override
int get hashCode =>
recoverySecretShare.hashCode ^ delete.hashCode ^ threshold.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PasswordLessRecovery &&
runtimeType == other.runtimeType &&
recoverySecretShare == other.recoverySecretShare &&
delete == other.delete &&
threshold == other.threshold;
}

View file

@ -5,15 +5,11 @@
import '../api/server/prekeys.dart'; import '../api/server/prekeys.dart';
import '../frb_generated.dart'; import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'api.freezed.dart';
// These functions are ignored because they are not marked as `pub`: `from_rust_state` // These functions are ignored because they are not marked as `pub`: `api_result`, `empty_api_response`, `encoded_api_response`, `from_rust_state`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ApiConfig`, `ServerResult` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ApiConfig`, `ServerResult`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`
// These functions are ignored (category: IgnoreBecauseOwnerTyShouldIgnore): `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`, `into_bridge`
enum ApiConnectionState { enum ApiConnectionState {
stopped, stopped,
@ -85,11 +81,10 @@ class PreparedOutgoingMessage {
class RustApi { class RustApi {
const RustApi(); const RustApi();
static Future<ServerResultEmpty> addAdditionalUser({ static Future<void> addAdditionalUser({required PlatformInt64 userId}) =>
required PlatformInt64 userId, RustLib.instance.api.crateBridgeApiRustApiAddAdditionalUser(
}) => RustLib.instance.api.crateBridgeApiRustApiAddAdditionalUser( userId: userId,
userId: userId, );
);
static Future<BigInt> allocateSequence() => static Future<BigInt> allocateSequence() =>
RustLib.instance.api.crateBridgeApiRustApiAllocateSequence(); RustLib.instance.api.crateBridgeApiRustApiAllocateSequence();
@ -97,15 +92,15 @@ class RustApi {
static String apiBaseUrl({required String protocol}) => static String apiBaseUrl({required String protocol}) =>
RustLib.instance.api.crateBridgeApiRustApiApiBaseUrl(protocol: protocol); RustLib.instance.api.crateBridgeApiRustApiApiBaseUrl(protocol: protocol);
static Future<ServerResultEmpty> changeUsername({required String username}) => static Future<void> changeUsername({required String username}) => RustLib
RustLib.instance.api.crateBridgeApiRustApiChangeUsername( .instance
username: username, .api
); .crateBridgeApiRustApiChangeUsername(username: username);
static Future<void> checkForDeletedUsernames() => static Future<void> checkForDeletedUsernames() =>
RustLib.instance.api.crateBridgeApiRustApiCheckForDeletedUsernames(); RustLib.instance.api.crateBridgeApiRustApiCheckForDeletedUsernames();
static Future<ServerResultVecU8> checkForPasswordlessNotification({ static Future<Uint8List> checkForPasswordlessNotification({
required String notificationId, required String notificationId,
required List<int> downloadAuthToken, required List<int> downloadAuthToken,
required Int64List alreadyReceivedMessageIds, required Int64List alreadyReceivedMessageIds,
@ -119,11 +114,10 @@ class RustApi {
static Future<void> close() => static Future<void> close() =>
RustLib.instance.api.crateBridgeApiRustApiClose(); RustLib.instance.api.crateBridgeApiRustApiClose();
static Future<ServerResultEmpty> confirmMemoriesUpload({ static Future<void> confirmMemoriesUpload({required String mediaId}) =>
required String mediaId, RustLib.instance.api.crateBridgeApiRustApiConfirmMemoriesUpload(
}) => RustLib.instance.api.crateBridgeApiRustApiConfirmMemoriesUpload( mediaId: mediaId,
mediaId: mediaId, );
);
static Future<void> connect() => static Future<void> connect() =>
RustLib.instance.api.crateBridgeApiRustApiConnect(); RustLib.instance.api.crateBridgeApiRustApiConnect();
@ -131,25 +125,31 @@ class RustApi {
static Future<ApiConnectionState> connectionState() => static Future<ApiConnectionState> connectionState() =>
RustLib.instance.api.crateBridgeApiRustApiConnectionState(); RustLib.instance.api.crateBridgeApiRustApiConnectionState();
static Future<ServerResultEmpty> deleteAccount() => static Future<void> deleteAccount() =>
RustLib.instance.api.crateBridgeApiRustApiDeleteAccount(); RustLib.instance.api.crateBridgeApiRustApiDeleteAccount();
static Future<ServerResultEmpty> deleteMemory({required String mediaId}) => static Future<void> deleteMemory({required String mediaId}) =>
RustLib.instance.api.crateBridgeApiRustApiDeleteMemory(mediaId: mediaId); RustLib.instance.api.crateBridgeApiRustApiDeleteMemory(mediaId: mediaId);
static Future<ServerResultEmpty> disableMemoriesBackup() => static Future<void> disableMemoriesBackup() =>
RustLib.instance.api.crateBridgeApiRustApiDisableMemoriesBackup(); RustLib.instance.api.crateBridgeApiRustApiDisableMemoriesBackup();
static Future<ServerResultEmpty> downloadDone({required List<int> token}) => static Future<void> downloadDone({required List<int> token}) =>
RustLib.instance.api.crateBridgeApiRustApiDownloadDone(token: token); RustLib.instance.api.crateBridgeApiRustApiDownloadDone(token: token);
static Future<void> downloadMedia({required String mediaId}) =>
RustLib.instance.api.crateBridgeApiRustApiDownloadMedia(mediaId: mediaId);
static Future<void> downloadPendingMedia() =>
RustLib.instance.api.crateBridgeApiRustApiDownloadPendingMedia();
static Stream<ApiEvent> events() => static Stream<ApiEvent> events() =>
RustLib.instance.api.crateBridgeApiRustApiEvents(); RustLib.instance.api.crateBridgeApiRustApiEvents();
static Future<ServerResultEmpty> forceIpaCheck() => static Future<void> forceIpaCheck() =>
RustLib.instance.api.crateBridgeApiRustApiForceIpaCheck(); RustLib.instance.api.crateBridgeApiRustApiForceIpaCheck();
static Future<ServerResultVecU8> getMemoriesUrl({ static Future<Uint8List> getMemoriesUrl({
required String mediaId, required String mediaId,
required bool thumbnail, required bool thumbnail,
}) => RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUrl( }) => RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUrl(
@ -157,16 +157,16 @@ class RustApi {
thumbnail: thumbnail, thumbnail: thumbnail,
); );
static Future<ServerResultVecU8> getMemoriesUsage() => static Future<Uint8List> getMemoriesUsage() =>
RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUsage(); RustLib.instance.api.crateBridgeApiRustApiGetMemoriesUsage();
static Future<Uint8List> getPlanBalance() => static Future<Uint8List> getPlanBalance() =>
RustLib.instance.api.crateBridgeApiRustApiGetPlanBalance(); RustLib.instance.api.crateBridgeApiRustApiGetPlanBalance();
static Future<ServerResultVecU8> getProofOfWork() => static Future<Uint8List> getProofOfWork() =>
RustLib.instance.api.crateBridgeApiRustApiGetProofOfWork(); RustLib.instance.api.crateBridgeApiRustApiGetProofOfWork();
static Future<ServerResultVecU8> getServerKeyForPasswordlessRecovery({ static Future<Uint8List> getServerKeyForPasswordlessRecovery({
required PlatformInt64 userId, required PlatformInt64 userId,
required List<int> serverKeyProtection, required List<int> serverKeyProtection,
Uint8List? pinUnlockToken, Uint8List? pinUnlockToken,
@ -181,14 +181,13 @@ class RustApi {
email: email, email: email,
); );
static Future<ServerResultVecU8> getUserById({ static Future<Uint8List> getUserById({required PlatformInt64 userId}) =>
required PlatformInt64 userId, RustLib.instance.api.crateBridgeApiRustApiGetUserById(userId: userId);
}) => RustLib.instance.api.crateBridgeApiRustApiGetUserById(userId: userId);
static Future<ServerResultVecU8> getUserData({required String username}) => static Future<Uint8List> getUserData({required String username}) =>
RustLib.instance.api.crateBridgeApiRustApiGetUserData(username: username); RustLib.instance.api.crateBridgeApiRustApiGetUserData(username: username);
static Future<ServerResultI64> getUserIdFromUsername({ static Future<PlatformInt64> getUserIdFromUsername({
required String username, required String username,
}) => RustLib.instance.api.crateBridgeApiRustApiGetUserIdFromUsername( }) => RustLib.instance.api.crateBridgeApiRustApiGetUserIdFromUsername(
username: username, username: username,
@ -230,7 +229,7 @@ class RustApi {
quoteMessageId: quoteMessageId, quoteMessageId: quoteMessageId,
); );
static Future<Uint8List> ipaPurchase({ static Future<void> ipaPurchase({
required String productId, required String productId,
required String source, required String source,
required String verificationData, required String verificationData,
@ -240,10 +239,8 @@ class RustApi {
verificationData: verificationData, verificationData: verificationData,
); );
static Future<Uint8List> loadPlanBalance({required bool useCache}) => RustLib static Future<Uint8List> loadPlanBalance() =>
.instance RustLib.instance.api.crateBridgeApiRustApiLoadPlanBalance();
.api
.crateBridgeApiRustApiLoadPlanBalance(useCache: useCache);
static Future<void> notifyMessagesOpened({ static Future<void> notifyMessagesOpened({
required PlatformInt64 contactId, required PlatformInt64 contactId,
@ -253,13 +250,18 @@ class RustApi {
messageIds: messageIds, messageIds: messageIds,
); );
static Future<void> performPasswordlessRecoveryHeartbeat() => RustLib
.instance
.api
.crateBridgeApiRustApiPerformPasswordlessRecoveryHeartbeat();
static Future<PreparedOutgoingMessage?> prepareQueuedMessage({ static Future<PreparedOutgoingMessage?> prepareQueuedMessage({
required String receiptId, required String receiptId,
}) => RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage( }) => RustLib.instance.api.crateBridgeApiRustApiPrepareQueuedMessage(
receiptId: receiptId, receiptId: receiptId,
); );
static Future<ServerResultI64> register({ static Future<PlatformInt64> register({
required String username, required String username,
required PlatformInt64 proofOfWork, required PlatformInt64 proofOfWork,
required String langCode, required String langCode,
@ -271,7 +273,7 @@ class RustApi {
isIos: isIos, isIos: isIos,
); );
static Future<ServerResultEmpty> registerPasswordlessNotification({ static Future<void> registerPasswordlessNotification({
required String notificationId, required String notificationId,
required List<int> downloadAuthToken, required List<int> downloadAuthToken,
required String langCode, required String langCode,
@ -284,7 +286,7 @@ class RustApi {
googleFcm: googleFcm, googleFcm: googleFcm,
); );
static Future<ServerResultEmpty> registerPasswordlessRecovery({ static Future<void> registerPasswordlessRecovery({
required List<int> encryptedServerKey, required List<int> encryptedServerKey,
Uint8List? pinUnlockToken, Uint8List? pinUnlockToken,
}) => RustLib.instance.api.crateBridgeApiRustApiRegisterPasswordlessRecovery( }) => RustLib.instance.api.crateBridgeApiRustApiRegisterPasswordlessRecovery(
@ -297,13 +299,12 @@ class RustApi {
static Future<void> reloadConfiguration() => static Future<void> reloadConfiguration() =>
RustLib.instance.api.crateBridgeApiRustApiReloadConfiguration(); RustLib.instance.api.crateBridgeApiRustApiReloadConfiguration();
static Future<ServerResultEmpty> removeAdditionalUser({ static Future<void> removeAdditionalUser({required PlatformInt64 userId}) =>
required PlatformInt64 userId, RustLib.instance.api.crateBridgeApiRustApiRemoveAdditionalUser(
}) => RustLib.instance.api.crateBridgeApiRustApiRemoveAdditionalUser( userId: userId,
userId: userId, );
);
static Future<ServerResultEmpty> reportUser({ static Future<void> reportUser({
required PlatformInt64 userId, required PlatformInt64 userId,
required String reason, required String reason,
}) => RustLib.instance.api.crateBridgeApiRustApiReportUser( }) => RustLib.instance.api.crateBridgeApiRustApiReportUser(
@ -321,7 +322,12 @@ class RustApi {
username: username, username: username,
); );
static Future<ServerResultVecU8> requestMemoriesUpload({ static Future<void> requestMediaReupload({required String mediaId}) => RustLib
.instance
.api
.crateBridgeApiRustApiRequestMediaReupload(mediaId: mediaId);
static Future<Uint8List> requestMemoriesUpload({
required PlatformInt64 size, required PlatformInt64 size,
required PlatformInt64 originalDate, required PlatformInt64 originalDate,
required String mediaId, required String mediaId,
@ -375,7 +381,7 @@ class RustApi {
.api .api
.crateBridgeApiRustApiSendQueuedMessage(receiptId: receiptId); .crateBridgeApiRustApiSendQueuedMessage(receiptId: receiptId);
static Future<ServerResultEmpty> sendTextMessage({ static Future<void> sendTextMessage({
required PlatformInt64 userId, required PlatformInt64 userId,
required List<int> body, required List<int> body,
Uint8List? pushData, Uint8List? pushData,
@ -398,7 +404,7 @@ class RustApi {
.api .api
.crateBridgeApiRustApiSetBackground(inBackground: inBackground); .crateBridgeApiRustApiSetBackground(inBackground: inBackground);
static Future<ServerResultEmpty> setLoginToken({required List<int> token}) => static Future<void> setLoginToken({required List<int> token}) =>
RustLib.instance.api.crateBridgeApiRustApiSetLoginToken(token: token); RustLib.instance.api.crateBridgeApiRustApiSetLoginToken(token: token);
static Future<void> setNetworkAvailable({required bool available}) => RustLib static Future<void> setNetworkAvailable({required bool available}) => RustLib
@ -406,7 +412,7 @@ class RustApi {
.api .api
.crateBridgeApiRustApiSetNetworkAvailable(available: available); .crateBridgeApiRustApiSetNetworkAvailable(available: available);
static Future<ServerResultEmpty> submitRecoveryShare({ static Future<void> submitRecoveryShare({
required String notificationId, required String notificationId,
required List<int> encryptedMessage, required List<int> encryptedMessage,
}) => RustLib.instance.api.crateBridgeApiRustApiSubmitRecoveryShare( }) => RustLib.instance.api.crateBridgeApiRustApiSubmitRecoveryShare(
@ -414,10 +420,10 @@ class RustApi {
encryptedMessage: encryptedMessage, encryptedMessage: encryptedMessage,
); );
static Future<ServerResultEmpty> updateFcmToken({required String token}) => static Future<void> updateFcmToken({required String token}) =>
RustLib.instance.api.crateBridgeApiRustApiUpdateFcmToken(token: token); RustLib.instance.api.crateBridgeApiRustApiUpdateFcmToken(token: token);
static Future<Uint8List> updateSignedPreKey({ static Future<void> updateSignedPreKey({
required PlatformInt64 id, required PlatformInt64 id,
required List<int> key, required List<int> key,
required List<int> signature, required List<int> signature,
@ -427,7 +433,7 @@ class RustApi {
signature: signature, signature: signature,
); );
static Future<Uint8List> uploadPqcPreKeys({ static Future<void> uploadPqcPreKeys({
required PlatformInt64 eccSignedPrekeyId, required PlatformInt64 eccSignedPrekeyId,
required List<int> eccSignedPrekey, required List<int> eccSignedPrekey,
required List<int> eccSignedPrekeySignature, required List<int> eccSignedPrekeySignature,
@ -453,37 +459,3 @@ class RustApi {
identical(this, other) || identical(this, other) ||
other is RustApi && runtimeType == other.runtimeType; other is RustApi && runtimeType == other.runtimeType;
} }
@freezed
sealed class ServerResultEmpty with _$ServerResultEmpty {
const ServerResultEmpty._();
const factory ServerResultEmpty.ok() = ServerResultEmpty_Ok;
const factory ServerResultEmpty.errorCode(
int field0,
) = ServerResultEmpty_ErrorCode;
}
@freezed
sealed class ServerResultI64 with _$ServerResultI64 {
const ServerResultI64._();
const factory ServerResultI64.ok(
PlatformInt64 field0,
) = ServerResultI64_Ok;
const factory ServerResultI64.errorCode(
int field0,
) = ServerResultI64_ErrorCode;
}
@freezed
sealed class ServerResultVecU8 with _$ServerResultVecU8 {
const ServerResultVecU8._();
const factory ServerResultVecU8.ok(
Uint8List field0,
) = ServerResultVecU8_Ok;
const factory ServerResultVecU8.errorCode(
int field0,
) = ServerResultVecU8_ErrorCode;
}

View file

@ -4,7 +4,7 @@
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import // ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart'; import '../frb_generated.dart';
import '../user_config.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_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_callbacks` // These functions are ignored because they are not marked as `pub`: `get_callbacks`
@ -28,19 +28,13 @@ Future<void> initFlutterCallbacks({
required FutureOr<List<LegacySignalPreKey>> Function() required FutureOr<List<LegacySignalPreKey>> Function()
legacySignalGeneratePrekeys, legacySignalGeneratePrekeys,
required FutureOr<void> Function(PlatformInt64) apiResyncSignalSession, required FutureOr<void> Function(PlatformInt64) apiResyncSignalSession,
required FutureOr<void> Function(PlatformInt64) apiPushKeyRequested,
required FutureOr<void> Function(PlatformInt64, String, String)
apiGroupMembershipError,
required FutureOr<void> Function(String, String, PlatformInt64, String) required FutureOr<void> Function(String, String, PlatformInt64, String)
apiMediaAction, apiMediaAction,
required FutureOr<void> Function(PlatformInt64, Uint8List) required FutureOr<void> Function(PlatformInt64, Uint8List)
apiVerificationProof, apiVerificationProof,
required FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int)
apiCreatePushData,
required FutureOr<void> Function(PlatformInt64) apiCreatePushAvatars, required FutureOr<void> Function(PlatformInt64) apiCreatePushAvatars,
required FutureOr<void> Function() apiRecoveryChanged,
required FutureOr<void> Function(String, PlatformInt64) apiMediaReceived, required FutureOr<void> Function(String, PlatformInt64) apiMediaReceived,
required FutureOr<void> Function(String, bool) apiGroupStateRefresh, required FutureOr<void> Function(UserConfig) apiUserConfigChanged,
}) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks( }) => RustLib.instance.api.crateBridgeCallbacksInitFlutterCallbacks(
callbackId: callbackId, callbackId: callbackId,
loggingGetStreamSink: loggingGetStreamSink, loggingGetStreamSink: loggingGetStreamSink,
@ -48,15 +42,11 @@ Future<void> initFlutterCallbacks({
legacySignalEncrypt: legacySignalEncrypt, legacySignalEncrypt: legacySignalEncrypt,
legacySignalGeneratePrekeys: legacySignalGeneratePrekeys, legacySignalGeneratePrekeys: legacySignalGeneratePrekeys,
apiResyncSignalSession: apiResyncSignalSession, apiResyncSignalSession: apiResyncSignalSession,
apiPushKeyRequested: apiPushKeyRequested,
apiGroupMembershipError: apiGroupMembershipError,
apiMediaAction: apiMediaAction, apiMediaAction: apiMediaAction,
apiVerificationProof: apiVerificationProof, apiVerificationProof: apiVerificationProof,
apiCreatePushData: apiCreatePushData,
apiCreatePushAvatars: apiCreatePushAvatars, apiCreatePushAvatars: apiCreatePushAvatars,
apiRecoveryChanged: apiRecoveryChanged,
apiMediaReceived: apiMediaReceived, apiMediaReceived: apiMediaReceived,
apiGroupStateRefresh: apiGroupStateRefresh, apiUserConfigChanged: apiUserConfigChanged,
); );
class LegacySignalDecryptResult { class LegacySignalDecryptResult {

View file

@ -4,7 +4,6 @@
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import // ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart'; import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
Future<bool> createNewGroup({ Future<bool> createNewGroup({

View file

@ -0,0 +1,58 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import '../user_config.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class UserConfigApi {
const UserConfigApi();
static UserConfig clone({required UserConfig config}) => RustLib.instance.api
.crateBridgeUserConfigUserConfigApiClone(config: config);
static Future<UserConfig> create({
required PlatformInt64 userId,
required String username,
required String displayName,
String? currentSetupPage,
required PlatformInt64 appVersion,
}) => RustLib.instance.api.crateBridgeUserConfigUserConfigApiCreate(
userId: userId,
username: username,
displayName: displayName,
currentSetupPage: currentSetupPage,
appVersion: appVersion,
);
static Future<UserConfig> importJson({required String json}) => RustLib
.instance
.api
.crateBridgeUserConfigUserConfigApiImportJson(json: json);
static Future<UserConfig?> load() =>
RustLib.instance.api.crateBridgeUserConfigUserConfigApiLoad();
static Future<UserConfig> save({required UserConfig config}) => RustLib
.instance
.api
.crateBridgeUserConfigUserConfigApiSave(config: config);
static Future<UserConfig> update({
required UserConfig base,
required UserConfig config,
}) => RustLib.instance.api.crateBridgeUserConfigUserConfigApiUpdate(
base: base,
config: config,
);
@override
int get hashCode => 0;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserConfigApi && runtimeType == other.runtimeType;
}

View file

@ -9,62 +9,25 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class FlutterUserDiscovery { class FlutterUserDiscovery {
const FlutterUserDiscovery(); const FlutterUserDiscovery();
static Future<void> changeExclusionForContact({
required int callbackId,
required PlatformInt64 contactId,
required bool exclude,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryChangeExclusionForContact(
callbackId: callbackId,
contactId: contactId,
exclude: exclude,
);
/// UI-facing read used to display the current discovery version.
static Future<Uint8List> getCurrentVersion({required int callbackId}) => static Future<Uint8List> getCurrentVersion({required int callbackId}) =>
RustLib.instance.api RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion( .crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetCurrentVersion(
callbackId: callbackId, callbackId: callbackId,
); );
static Future<List<Uint8List>> getNewMessages({ /// UI-facing hook used when a user manually changes contact verification.
required int callbackId,
required PlatformInt64 contactId,
required List<int> receivedVersion,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryGetNewMessages(
callbackId: callbackId,
contactId: contactId,
receivedVersion: receivedVersion,
);
static Future<void> handleNewMessages({
required int callbackId,
required PlatformInt64 contactId,
PlatformInt64? publicKeyVerifiedTimestamp,
required List<Uint8List> messages,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryHandleNewMessages(
callbackId: callbackId,
contactId: contactId,
publicKeyVerifiedTimestamp: publicKeyVerifiedTimestamp,
messages: messages,
);
static Future<void> initializeOrUpdate({
required int callbackId,
required int threshold,
required PlatformInt64 userId,
required List<int> publicKey,
required bool sharePromotion,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryInitializeOrUpdate(
callbackId: callbackId,
threshold: threshold,
userId: userId,
publicKey: publicKey,
sharePromotion: sharePromotion,
);
static Future<Uint8List?> shouldRequestNewMessages({
required int callbackId,
required PlatformInt64 contactId,
required List<int> version,
}) => RustLib.instance.api
.crateBridgeWrapperUserDiscoveryFlutterUserDiscoveryShouldRequestNewMessages(
callbackId: callbackId,
contactId: contactId,
version: version,
);
static Future<void> updateVerificationStateForUser({ static Future<void> updateVerificationStateForUser({
required int callbackId, required int callbackId,
required PlatformInt64 contactId, required PlatformInt64 contactId,

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@ import 'bridge.dart';
import 'bridge/api.dart'; import 'bridge/api.dart';
import 'bridge/callbacks.dart'; import 'bridge/callbacks.dart';
import 'bridge/groups.dart'; import 'bridge/groups.dart';
import 'bridge/user_config.dart';
import 'bridge/wrapper.dart'; import 'bridge/wrapper.dart';
import 'bridge/wrapper/app_database.dart'; import 'bridge/wrapper/app_database.dart';
import 'bridge/wrapper/backup.dart'; import 'bridge/wrapper/backup.dart';
@ -23,6 +24,7 @@ import 'keys/backup_password_keys.dart';
import 'lib.dart'; import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
import 'signal/engine.dart'; import 'signal/engine.dart';
import 'user_config.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> { abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({ RustLibApiImplPlatform({
@ -35,16 +37,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException dco_decode_AnyhowException(dynamic raw); AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
DateTime dco_decode_Chrono_Utc(dynamic raw);
@protected @protected
FutureOr<void> Function(String, String, PlatformInt64, String) FutureOr<void> Function(String, String, PlatformInt64, String)
dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
dynamic raw, dynamic raw,
); );
@protected
FutureOr<void> Function(String, bool)
dco_decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(dynamic raw);
@protected @protected
FutureOr<void> Function(String, PlatformInt64) FutureOr<void> Function(String, PlatformInt64)
dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw); dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw);
@ -61,20 +62,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
FutureOr<void> Function()
dco_decode_DartFn_Inputs__Output_unit_AnyhowException(dynamic raw);
@protected @protected
FutureOr<void> Function(PlatformInt64) FutureOr<void> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw); dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
@protected
FutureOr<void> Function(PlatformInt64, String, String)
dco_decode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(
dynamic raw,
);
@protected @protected
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List) FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
@ -94,14 +85,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int) FutureOr<void> Function(UserConfig)
dco_decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw);
dynamic raw,
);
@protected @protected
Object dco_decode_DartOpaque(dynamic raw); Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
@protected @protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None( Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw, dynamic raw,
@ -131,6 +123,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
bool dco_decode_bool(dynamic raw); bool dco_decode_bool(dynamic raw);
@protected
DateTime dco_decode_box_autoadd_Chrono_Utc(dynamic raw);
@protected @protected
ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw); ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw);
@ -157,14 +152,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
PasswordlessRecoveryConfig
dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw);
@protected @protected
PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message(
dynamic raw, dynamic raw,
); );
@protected
TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw);
@protected @protected
int dco_decode_box_autoadd_u_32(dynamic raw); int dco_decode_box_autoadd_u_32(dynamic raw);
@protected
UserConfig dco_decode_box_autoadd_user_config(dynamic raw);
@protected @protected
double dco_decode_f_64(dynamic raw); double dco_decode_f_64(dynamic raw);
@ -189,6 +194,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 dco_decode_isize(dynamic raw); PlatformInt64 dco_decode_isize(dynamic raw);
@protected
LastBackupUploadState dco_decode_last_backup_upload_state(dynamic raw);
@protected @protected
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw); LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
@ -243,15 +251,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
List<(PlatformInt64, Uint8List)> List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw); dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
List<(String, List<String>)> dco_decode_list_record_string_list_string(
dynamic raw,
);
@protected @protected
List<SqlRow> dco_decode_list_sql_row(dynamic raw); List<SqlRow> dco_decode_list_sql_row(dynamic raw);
@protected @protected
List<SqlValue> dco_decode_list_sql_value(dynamic raw); List<SqlValue> dco_decode_list_sql_value(dynamic raw);
@protected
Map<String, List<String>>? dco_decode_opt_Map_String_list_String_None(
dynamic raw,
);
@protected @protected
String? dco_decode_opt_String(dynamic raw); String? dco_decode_opt_String(dynamic raw);
@protected
DateTime? dco_decode_opt_box_autoadd_Chrono_Utc(dynamic raw);
@protected @protected
ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state( ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state(
dynamic raw, dynamic raw,
@ -270,17 +291,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
LegacySignalEncryptResult? LegacySignalEncryptResult?
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw); dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
@protected
PasswordlessRecoveryConfig?
dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw);
@protected @protected
PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message(
dynamic raw, dynamic raw,
); );
@protected
TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw);
@protected @protected
int? dco_decode_opt_box_autoadd_u_32(dynamic raw); int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
@protected
UserConfig? dco_decode_opt_box_autoadd_user_config(dynamic raw);
@protected
List<String>? dco_decode_opt_list_String(dynamic raw);
@protected @protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
PasswordlessRecoveryConfig dco_decode_passwordless_recovery_config(
dynamic raw,
);
@protected @protected
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw); PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
@ -297,6 +336,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
@protected @protected
(String, String) dco_decode_record_string_string(dynamic raw); (String, String) dco_decode_record_string_string(dynamic raw);
@ -322,13 +364,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustUtils dco_decode_rust_utils(dynamic raw); RustUtils dco_decode_rust_utils(dynamic raw);
@protected @protected
ServerResultEmpty dco_decode_server_result_empty(dynamic raw); SetupProfile dco_decode_setup_profile(dynamic raw);
@protected
ServerResultI64 dco_decode_server_result_i_64(dynamic raw);
@protected
ServerResultVecU8 dco_decode_server_result_vec_u_8(dynamic raw);
@protected @protected
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw); SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
@ -342,6 +378,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
SqlValue dco_decode_sql_value(dynamic raw); SqlValue dco_decode_sql_value(dynamic raw);
@protected
ThemeMode dco_decode_theme_mode(dynamic raw);
@protected
TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw);
@protected @protected
int dco_decode_u_32(dynamic raw); int dco_decode_u_32(dynamic raw);
@ -357,15 +399,29 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void dco_decode_unit(dynamic raw); void dco_decode_unit(dynamic raw);
@protected
UserConfig dco_decode_user_config(dynamic raw);
@protected
UserConfigApi dco_decode_user_config_api(dynamic raw);
@protected @protected
BigInt dco_decode_usize(dynamic raw); BigInt dco_decode_usize(dynamic raw);
@protected @protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
DateTime sse_decode_Chrono_Utc(SseDeserializer deserializer);
@protected @protected
Object sse_decode_DartOpaque(SseDeserializer deserializer); Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<String, List<String>> sse_decode_Map_String_list_String_None(
SseDeserializer deserializer,
);
@protected @protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None( Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -403,6 +459,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
bool sse_decode_bool(SseDeserializer deserializer); bool sse_decode_bool(SseDeserializer deserializer);
@protected
DateTime sse_decode_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
@protected @protected
ApiConnectionState sse_decode_box_autoadd_api_connection_state( ApiConnectionState sse_decode_box_autoadd_api_connection_state(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -435,14 +494,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
PasswordlessRecoveryConfig
sse_decode_box_autoadd_passwordless_recovery_config(
SseDeserializer deserializer,
);
@protected @protected
PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup(
SseDeserializer deserializer,
);
@protected @protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
@protected
UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer);
@protected @protected
double sse_decode_f_64(SseDeserializer deserializer); double sse_decode_f_64(SseDeserializer deserializer);
@ -469,6 +542,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer); PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
LastBackupUploadState sse_decode_last_backup_upload_state(
SseDeserializer deserializer,
);
@protected @protected
LegacyMigrationReport sse_decode_legacy_migration_report( LegacyMigrationReport sse_decode_legacy_migration_report(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -537,15 +615,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
List<(String, List<String>)> sse_decode_list_record_string_list_string(
SseDeserializer deserializer,
);
@protected @protected
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer); List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
@protected @protected
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer); List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
@protected
Map<String, List<String>>? sse_decode_opt_Map_String_list_String_None(
SseDeserializer deserializer,
);
@protected @protected
String? sse_decode_opt_String(SseDeserializer deserializer); String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
DateTime? sse_decode_opt_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
@protected @protected
ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state( ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -566,17 +657,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
PasswordlessRecoveryConfig?
sse_decode_opt_box_autoadd_passwordless_recovery_config(
SseDeserializer deserializer,
);
@protected @protected
PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup(
SseDeserializer deserializer,
);
@protected @protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
@protected
UserConfig? sse_decode_opt_box_autoadd_user_config(
SseDeserializer deserializer,
);
@protected
List<String>? sse_decode_opt_list_String(SseDeserializer deserializer);
@protected @protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
PasswordlessRecoveryConfig sse_decode_passwordless_recovery_config(
SseDeserializer deserializer,
);
@protected @protected
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer); PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
@ -595,6 +710,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
(String, List<String>) sse_decode_record_string_list_string(
SseDeserializer deserializer,
);
@protected @protected
(String, String) sse_decode_record_string_string( (String, String) sse_decode_record_string_string(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -626,17 +746,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustUtils sse_decode_rust_utils(SseDeserializer deserializer); RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
@protected @protected
ServerResultEmpty sse_decode_server_result_empty( SetupProfile sse_decode_setup_profile(SseDeserializer deserializer);
SseDeserializer deserializer,
);
@protected
ServerResultI64 sse_decode_server_result_i_64(SseDeserializer deserializer);
@protected
ServerResultVecU8 sse_decode_server_result_vec_u_8(
SseDeserializer deserializer,
);
@protected @protected
SqlExecutionResult sse_decode_sql_execution_result( SqlExecutionResult sse_decode_sql_execution_result(
@ -652,6 +762,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
SqlValue sse_decode_sql_value(SseDeserializer deserializer); SqlValue sse_decode_sql_value(SseDeserializer deserializer);
@protected
ThemeMode sse_decode_theme_mode(SseDeserializer deserializer);
@protected
TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer);
@protected @protected
int sse_decode_u_32(SseDeserializer deserializer); int sse_decode_u_32(SseDeserializer deserializer);
@ -667,6 +783,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_decode_unit(SseDeserializer deserializer); void sse_decode_unit(SseDeserializer deserializer);
@protected
UserConfig sse_decode_user_config(SseDeserializer deserializer);
@protected
UserConfigApi sse_decode_user_config_api(SseDeserializer deserializer);
@protected @protected
BigInt sse_decode_usize(SseDeserializer deserializer); BigInt sse_decode_usize(SseDeserializer deserializer);
@ -676,6 +798,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer);
@protected @protected
void void
sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
@ -683,12 +808,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(
FutureOr<void> Function(String, bool) self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException( void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(
FutureOr<void> Function(String, PlatformInt64) self, FutureOr<void> Function(String, PlatformInt64) self,
@ -708,24 +827,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_DartFn_Inputs__Output_unit_AnyhowException(
FutureOr<void> Function() self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
FutureOr<void> Function(PlatformInt64) self, FutureOr<void> Function(PlatformInt64) self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(
FutureOr<void> Function(PlatformInt64, String, String) self,
SseSerializer serializer,
);
@protected @protected
void void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
@ -750,15 +857,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( FutureOr<void> Function(UserConfig) self,
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int) self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected @protected
void sse_encode_DartOpaque(Object self, SseSerializer serializer); void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_Map_String_list_String_None(
Map<String, List<String>> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None( void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self, Map<PlatformInt64, Uint8List> self,
@ -801,6 +913,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_bool(bool self, SseSerializer serializer); void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_Chrono_Utc(
DateTime self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_api_connection_state( void sse_encode_box_autoadd_api_connection_state(
ApiConnectionState self, ApiConnectionState self,
@ -843,15 +961,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_passwordless_recovery_config(
PasswordlessRecoveryConfig self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_prepared_outgoing_message( void sse_encode_box_autoadd_prepared_outgoing_message(
PreparedOutgoingMessage self, PreparedOutgoingMessage self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_twonly_safe_backup(
TwonlySafeBackup self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_user_config(
UserConfig self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_f_64(double self, SseSerializer serializer); void sse_encode_f_64(double self, SseSerializer serializer);
@ -882,6 +1018,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_last_backup_upload_state(
LastBackupUploadState self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_legacy_migration_report( void sse_encode_legacy_migration_report(
LegacyMigrationReport self, LegacyMigrationReport self,
@ -966,15 +1108,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_list_record_string_list_string(
List<(String, List<String>)> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer); void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
@protected @protected
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer); void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
@protected
void sse_encode_opt_Map_String_list_String_None(
Map<String, List<String>>? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_String(String? self, SseSerializer serializer); void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_Chrono_Utc(
DateTime? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_api_connection_state( void sse_encode_opt_box_autoadd_api_connection_state(
ApiConnectionState? self, ApiConnectionState? self,
@ -999,21 +1159,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_opt_box_autoadd_passwordless_recovery_config(
PasswordlessRecoveryConfig? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_prepared_outgoing_message( void sse_encode_opt_box_autoadd_prepared_outgoing_message(
PreparedOutgoingMessage? self, PreparedOutgoingMessage? self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_opt_box_autoadd_twonly_safe_backup(
TwonlySafeBackup? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_user_config(
UserConfig? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_String(List<String>? self, SseSerializer serializer);
@protected @protected
void sse_encode_opt_list_prim_u_8_strict( void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self, Uint8List? self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_passwordless_recovery_config(
PasswordlessRecoveryConfig self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_pqc_pre_key_input( void sse_encode_pqc_pre_key_input(
PqcPreKeyInput self, PqcPreKeyInput self,
@ -1038,6 +1225,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_record_string_list_string(
(String, List<String>) self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_record_string_string( void sse_encode_record_string_string(
(String, String) self, (String, String) self,
@ -1078,22 +1271,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer); void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
@protected @protected
void sse_encode_server_result_empty( void sse_encode_setup_profile(SetupProfile self, SseSerializer serializer);
ServerResultEmpty self,
SseSerializer serializer,
);
@protected
void sse_encode_server_result_i_64(
ServerResultI64 self,
SseSerializer serializer,
);
@protected
void sse_encode_server_result_vec_u_8(
ServerResultVecU8 self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_sql_execution_result( void sse_encode_sql_execution_result(
@ -1110,6 +1288,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_sql_value(SqlValue self, SseSerializer serializer); void sse_encode_sql_value(SqlValue self, SseSerializer serializer);
@protected
void sse_encode_theme_mode(ThemeMode self, SseSerializer serializer);
@protected
void sse_encode_twonly_safe_backup(
TwonlySafeBackup self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_u_32(int self, SseSerializer serializer); void sse_encode_u_32(int self, SseSerializer serializer);
@ -1125,6 +1312,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_unit(void self, SseSerializer serializer); void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_user_config(UserConfig self, SseSerializer serializer);
@protected
void sse_encode_user_config_api(UserConfigApi self, SseSerializer serializer);
@protected @protected
void sse_encode_usize(BigInt self, SseSerializer serializer); void sse_encode_usize(BigInt self, SseSerializer serializer);
} }

View file

@ -11,6 +11,7 @@ import 'bridge.dart';
import 'bridge/api.dart'; import 'bridge/api.dart';
import 'bridge/callbacks.dart'; import 'bridge/callbacks.dart';
import 'bridge/groups.dart'; import 'bridge/groups.dart';
import 'bridge/user_config.dart';
import 'bridge/wrapper.dart'; import 'bridge/wrapper.dart';
import 'bridge/wrapper/app_database.dart'; import 'bridge/wrapper/app_database.dart';
import 'bridge/wrapper/backup.dart'; import 'bridge/wrapper/backup.dart';
@ -25,6 +26,7 @@ import 'keys/backup_password_keys.dart';
import 'lib.dart'; import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
import 'signal/engine.dart'; import 'signal/engine.dart';
import 'user_config.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> { abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustLibApiImplPlatform({ RustLibApiImplPlatform({
@ -37,16 +39,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
AnyhowException dco_decode_AnyhowException(dynamic raw); AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
DateTime dco_decode_Chrono_Utc(dynamic raw);
@protected @protected
FutureOr<void> Function(String, String, PlatformInt64, String) FutureOr<void> Function(String, String, PlatformInt64, String)
dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( dco_decode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
dynamic raw, dynamic raw,
); );
@protected
FutureOr<void> Function(String, bool)
dco_decode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(dynamic raw);
@protected @protected
FutureOr<void> Function(String, PlatformInt64) FutureOr<void> Function(String, PlatformInt64)
dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw); dco_decode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(dynamic raw);
@ -63,20 +64,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
FutureOr<void> Function()
dco_decode_DartFn_Inputs__Output_unit_AnyhowException(dynamic raw);
@protected @protected
FutureOr<void> Function(PlatformInt64) FutureOr<void> Function(PlatformInt64)
dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw); dco_decode_DartFn_Inputs_i_64_Output_unit_AnyhowException(dynamic raw);
@protected
FutureOr<void> Function(PlatformInt64, String, String)
dco_decode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(
dynamic raw,
);
@protected @protected
FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List) FutureOr<LegacySignalEncryptResult?> Function(PlatformInt64, Uint8List)
dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( dco_decode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
@ -96,14 +87,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int) FutureOr<void> Function(UserConfig)
dco_decode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( dco_decode_DartFn_Inputs_user_config_Output_unit_AnyhowException(dynamic raw);
dynamic raw,
);
@protected @protected
Object dco_decode_DartOpaque(dynamic raw); Object dco_decode_DartOpaque(dynamic raw);
@protected
Map<String, List<String>> dco_decode_Map_String_list_String_None(dynamic raw);
@protected @protected
Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None( Map<PlatformInt64, Uint8List> dco_decode_Map_i_64_list_prim_u_8_strict_None(
dynamic raw, dynamic raw,
@ -133,6 +125,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
bool dco_decode_bool(dynamic raw); bool dco_decode_bool(dynamic raw);
@protected
DateTime dco_decode_box_autoadd_Chrono_Utc(dynamic raw);
@protected @protected
ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw); ApiConnectionState dco_decode_box_autoadd_api_connection_state(dynamic raw);
@ -159,14 +154,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
PasswordlessRecoveryConfig
dco_decode_box_autoadd_passwordless_recovery_config(dynamic raw);
@protected @protected
PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage dco_decode_box_autoadd_prepared_outgoing_message(
dynamic raw, dynamic raw,
); );
@protected
TwonlySafeBackup dco_decode_box_autoadd_twonly_safe_backup(dynamic raw);
@protected @protected
int dco_decode_box_autoadd_u_32(dynamic raw); int dco_decode_box_autoadd_u_32(dynamic raw);
@protected
UserConfig dco_decode_box_autoadd_user_config(dynamic raw);
@protected @protected
double dco_decode_f_64(dynamic raw); double dco_decode_f_64(dynamic raw);
@ -191,6 +196,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 dco_decode_isize(dynamic raw); PlatformInt64 dco_decode_isize(dynamic raw);
@protected
LastBackupUploadState dco_decode_last_backup_upload_state(dynamic raw);
@protected @protected
LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw); LegacyMigrationReport dco_decode_legacy_migration_report(dynamic raw);
@ -245,15 +253,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
List<(PlatformInt64, Uint8List)> List<(PlatformInt64, Uint8List)>
dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw); dco_decode_list_record_i_64_list_prim_u_8_strict(dynamic raw);
@protected
List<(String, List<String>)> dco_decode_list_record_string_list_string(
dynamic raw,
);
@protected @protected
List<SqlRow> dco_decode_list_sql_row(dynamic raw); List<SqlRow> dco_decode_list_sql_row(dynamic raw);
@protected @protected
List<SqlValue> dco_decode_list_sql_value(dynamic raw); List<SqlValue> dco_decode_list_sql_value(dynamic raw);
@protected
Map<String, List<String>>? dco_decode_opt_Map_String_list_String_None(
dynamic raw,
);
@protected @protected
String? dco_decode_opt_String(dynamic raw); String? dco_decode_opt_String(dynamic raw);
@protected
DateTime? dco_decode_opt_box_autoadd_Chrono_Utc(dynamic raw);
@protected @protected
ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state( ApiConnectionState? dco_decode_opt_box_autoadd_api_connection_state(
dynamic raw, dynamic raw,
@ -272,17 +293,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
LegacySignalEncryptResult? LegacySignalEncryptResult?
dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw); dco_decode_opt_box_autoadd_legacy_signal_encrypt_result(dynamic raw);
@protected
PasswordlessRecoveryConfig?
dco_decode_opt_box_autoadd_passwordless_recovery_config(dynamic raw);
@protected @protected
PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage? dco_decode_opt_box_autoadd_prepared_outgoing_message(
dynamic raw, dynamic raw,
); );
@protected
TwonlySafeBackup? dco_decode_opt_box_autoadd_twonly_safe_backup(dynamic raw);
@protected @protected
int? dco_decode_opt_box_autoadd_u_32(dynamic raw); int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
@protected
UserConfig? dco_decode_opt_box_autoadd_user_config(dynamic raw);
@protected
List<String>? dco_decode_opt_list_String(dynamic raw);
@protected @protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
PasswordlessRecoveryConfig dco_decode_passwordless_recovery_config(
dynamic raw,
);
@protected @protected
PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw); PqcPreKeyInput dco_decode_pqc_pre_key_input(dynamic raw);
@ -299,6 +338,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw, dynamic raw,
); );
@protected
(String, List<String>) dco_decode_record_string_list_string(dynamic raw);
@protected @protected
(String, String) dco_decode_record_string_string(dynamic raw); (String, String) dco_decode_record_string_string(dynamic raw);
@ -324,13 +366,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustUtils dco_decode_rust_utils(dynamic raw); RustUtils dco_decode_rust_utils(dynamic raw);
@protected @protected
ServerResultEmpty dco_decode_server_result_empty(dynamic raw); SetupProfile dco_decode_setup_profile(dynamic raw);
@protected
ServerResultI64 dco_decode_server_result_i_64(dynamic raw);
@protected
ServerResultVecU8 dco_decode_server_result_vec_u_8(dynamic raw);
@protected @protected
SqlExecutionResult dco_decode_sql_execution_result(dynamic raw); SqlExecutionResult dco_decode_sql_execution_result(dynamic raw);
@ -344,6 +380,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
SqlValue dco_decode_sql_value(dynamic raw); SqlValue dco_decode_sql_value(dynamic raw);
@protected
ThemeMode dco_decode_theme_mode(dynamic raw);
@protected
TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw);
@protected @protected
int dco_decode_u_32(dynamic raw); int dco_decode_u_32(dynamic raw);
@ -359,15 +401,29 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void dco_decode_unit(dynamic raw); void dco_decode_unit(dynamic raw);
@protected
UserConfig dco_decode_user_config(dynamic raw);
@protected
UserConfigApi dco_decode_user_config_api(dynamic raw);
@protected @protected
BigInt dco_decode_usize(dynamic raw); BigInt dco_decode_usize(dynamic raw);
@protected @protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
DateTime sse_decode_Chrono_Utc(SseDeserializer deserializer);
@protected @protected
Object sse_decode_DartOpaque(SseDeserializer deserializer); Object sse_decode_DartOpaque(SseDeserializer deserializer);
@protected
Map<String, List<String>> sse_decode_Map_String_list_String_None(
SseDeserializer deserializer,
);
@protected @protected
Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None( Map<PlatformInt64, Uint8List> sse_decode_Map_i_64_list_prim_u_8_strict_None(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -405,6 +461,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
bool sse_decode_bool(SseDeserializer deserializer); bool sse_decode_bool(SseDeserializer deserializer);
@protected
DateTime sse_decode_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
@protected @protected
ApiConnectionState sse_decode_box_autoadd_api_connection_state( ApiConnectionState sse_decode_box_autoadd_api_connection_state(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -437,14 +496,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
PasswordlessRecoveryConfig
sse_decode_box_autoadd_passwordless_recovery_config(
SseDeserializer deserializer,
);
@protected @protected
PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage sse_decode_box_autoadd_prepared_outgoing_message(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
TwonlySafeBackup sse_decode_box_autoadd_twonly_safe_backup(
SseDeserializer deserializer,
);
@protected @protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
@protected
UserConfig sse_decode_box_autoadd_user_config(SseDeserializer deserializer);
@protected @protected
double sse_decode_f_64(SseDeserializer deserializer); double sse_decode_f_64(SseDeserializer deserializer);
@ -471,6 +544,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
PlatformInt64 sse_decode_isize(SseDeserializer deserializer); PlatformInt64 sse_decode_isize(SseDeserializer deserializer);
@protected
LastBackupUploadState sse_decode_last_backup_upload_state(
SseDeserializer deserializer,
);
@protected @protected
LegacyMigrationReport sse_decode_legacy_migration_report( LegacyMigrationReport sse_decode_legacy_migration_report(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -539,15 +617,28 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
List<(String, List<String>)> sse_decode_list_record_string_list_string(
SseDeserializer deserializer,
);
@protected @protected
List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer); List<SqlRow> sse_decode_list_sql_row(SseDeserializer deserializer);
@protected @protected
List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer); List<SqlValue> sse_decode_list_sql_value(SseDeserializer deserializer);
@protected
Map<String, List<String>>? sse_decode_opt_Map_String_list_String_None(
SseDeserializer deserializer,
);
@protected @protected
String? sse_decode_opt_String(SseDeserializer deserializer); String? sse_decode_opt_String(SseDeserializer deserializer);
@protected
DateTime? sse_decode_opt_box_autoadd_Chrono_Utc(SseDeserializer deserializer);
@protected @protected
ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state( ApiConnectionState? sse_decode_opt_box_autoadd_api_connection_state(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -568,17 +659,41 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
PasswordlessRecoveryConfig?
sse_decode_opt_box_autoadd_passwordless_recovery_config(
SseDeserializer deserializer,
);
@protected @protected
PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message( PreparedOutgoingMessage? sse_decode_opt_box_autoadd_prepared_outgoing_message(
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
TwonlySafeBackup? sse_decode_opt_box_autoadd_twonly_safe_backup(
SseDeserializer deserializer,
);
@protected @protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
@protected
UserConfig? sse_decode_opt_box_autoadd_user_config(
SseDeserializer deserializer,
);
@protected
List<String>? sse_decode_opt_list_String(SseDeserializer deserializer);
@protected @protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
PasswordlessRecoveryConfig sse_decode_passwordless_recovery_config(
SseDeserializer deserializer,
);
@protected @protected
PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer); PqcPreKeyInput sse_decode_pqc_pre_key_input(SseDeserializer deserializer);
@ -597,6 +712,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer, SseDeserializer deserializer,
); );
@protected
(String, List<String>) sse_decode_record_string_list_string(
SseDeserializer deserializer,
);
@protected @protected
(String, String) sse_decode_record_string_string( (String, String) sse_decode_record_string_string(
SseDeserializer deserializer, SseDeserializer deserializer,
@ -628,17 +748,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
RustUtils sse_decode_rust_utils(SseDeserializer deserializer); RustUtils sse_decode_rust_utils(SseDeserializer deserializer);
@protected @protected
ServerResultEmpty sse_decode_server_result_empty( SetupProfile sse_decode_setup_profile(SseDeserializer deserializer);
SseDeserializer deserializer,
);
@protected
ServerResultI64 sse_decode_server_result_i_64(SseDeserializer deserializer);
@protected
ServerResultVecU8 sse_decode_server_result_vec_u_8(
SseDeserializer deserializer,
);
@protected @protected
SqlExecutionResult sse_decode_sql_execution_result( SqlExecutionResult sse_decode_sql_execution_result(
@ -654,6 +764,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
SqlValue sse_decode_sql_value(SseDeserializer deserializer); SqlValue sse_decode_sql_value(SseDeserializer deserializer);
@protected
ThemeMode sse_decode_theme_mode(SseDeserializer deserializer);
@protected
TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer);
@protected @protected
int sse_decode_u_32(SseDeserializer deserializer); int sse_decode_u_32(SseDeserializer deserializer);
@ -669,6 +785,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_decode_unit(SseDeserializer deserializer); void sse_decode_unit(SseDeserializer deserializer);
@protected
UserConfig sse_decode_user_config(SseDeserializer deserializer);
@protected
UserConfigApi sse_decode_user_config_api(SseDeserializer deserializer);
@protected @protected
BigInt sse_decode_usize(SseDeserializer deserializer); BigInt sse_decode_usize(SseDeserializer deserializer);
@ -678,6 +800,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_Chrono_Utc(DateTime self, SseSerializer serializer);
@protected @protected
void void
sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException( sse_encode_DartFn_Inputs_String_String_i_64_String_Output_unit_AnyhowException(
@ -685,12 +810,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_DartFn_Inputs_String_bool_Output_unit_AnyhowException(
FutureOr<void> Function(String, bool) self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException( void sse_encode_DartFn_Inputs_String_i_64_Output_unit_AnyhowException(
FutureOr<void> Function(String, PlatformInt64) self, FutureOr<void> Function(String, PlatformInt64) self,
@ -710,24 +829,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_DartFn_Inputs__Output_unit_AnyhowException(
FutureOr<void> Function() self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException( void sse_encode_DartFn_Inputs_i_64_Output_unit_AnyhowException(
FutureOr<void> Function(PlatformInt64) self, FutureOr<void> Function(PlatformInt64) self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_DartFn_Inputs_i_64_String_String_Output_unit_AnyhowException(
FutureOr<void> Function(PlatformInt64, String, String) self,
SseSerializer serializer,
);
@protected @protected
void void
sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException( sse_encode_DartFn_Inputs_i_64_list_prim_u_8_strict_Output_opt_box_autoadd_legacy_signal_encrypt_result_AnyhowException(
@ -752,15 +859,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
); );
@protected @protected
void void sse_encode_DartFn_Inputs_user_config_Output_unit_AnyhowException(
sse_encode_DartFn_Inputs_i_64_opt_String_list_prim_u_8_strict_i_32_Output_opt_list_prim_u_8_strict_AnyhowException( FutureOr<void> Function(UserConfig) self,
FutureOr<Uint8List?> Function(PlatformInt64, String?, Uint8List, int) self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected @protected
void sse_encode_DartOpaque(Object self, SseSerializer serializer); void sse_encode_DartOpaque(Object self, SseSerializer serializer);
@protected
void sse_encode_Map_String_list_String_None(
Map<String, List<String>> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_Map_i_64_list_prim_u_8_strict_None( void sse_encode_Map_i_64_list_prim_u_8_strict_None(
Map<PlatformInt64, Uint8List> self, Map<PlatformInt64, Uint8List> self,
@ -803,6 +915,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_bool(bool self, SseSerializer serializer); void sse_encode_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_Chrono_Utc(
DateTime self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_api_connection_state( void sse_encode_box_autoadd_api_connection_state(
ApiConnectionState self, ApiConnectionState self,
@ -845,15 +963,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_passwordless_recovery_config(
PasswordlessRecoveryConfig self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_prepared_outgoing_message( void sse_encode_box_autoadd_prepared_outgoing_message(
PreparedOutgoingMessage self, PreparedOutgoingMessage self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_box_autoadd_twonly_safe_backup(
TwonlySafeBackup self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_user_config(
UserConfig self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_f_64(double self, SseSerializer serializer); void sse_encode_f_64(double self, SseSerializer serializer);
@ -884,6 +1020,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); void sse_encode_isize(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_last_backup_upload_state(
LastBackupUploadState self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_legacy_migration_report( void sse_encode_legacy_migration_report(
LegacyMigrationReport self, LegacyMigrationReport self,
@ -968,15 +1110,33 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_list_record_string_list_string(
List<(String, List<String>)> self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer); void sse_encode_list_sql_row(List<SqlRow> self, SseSerializer serializer);
@protected @protected
void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer); void sse_encode_list_sql_value(List<SqlValue> self, SseSerializer serializer);
@protected
void sse_encode_opt_Map_String_list_String_None(
Map<String, List<String>>? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_String(String? self, SseSerializer serializer); void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_Chrono_Utc(
DateTime? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_api_connection_state( void sse_encode_opt_box_autoadd_api_connection_state(
ApiConnectionState? self, ApiConnectionState? self,
@ -1001,21 +1161,48 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_opt_box_autoadd_passwordless_recovery_config(
PasswordlessRecoveryConfig? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_prepared_outgoing_message( void sse_encode_opt_box_autoadd_prepared_outgoing_message(
PreparedOutgoingMessage? self, PreparedOutgoingMessage? self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_opt_box_autoadd_twonly_safe_backup(
TwonlySafeBackup? self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
@protected
void sse_encode_opt_box_autoadd_user_config(
UserConfig? self,
SseSerializer serializer,
);
@protected
void sse_encode_opt_list_String(List<String>? self, SseSerializer serializer);
@protected @protected
void sse_encode_opt_list_prim_u_8_strict( void sse_encode_opt_list_prim_u_8_strict(
Uint8List? self, Uint8List? self,
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_passwordless_recovery_config(
PasswordlessRecoveryConfig self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_pqc_pre_key_input( void sse_encode_pqc_pre_key_input(
PqcPreKeyInput self, PqcPreKeyInput self,
@ -1040,6 +1227,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer, SseSerializer serializer,
); );
@protected
void sse_encode_record_string_list_string(
(String, List<String>) self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_record_string_string( void sse_encode_record_string_string(
(String, String) self, (String, String) self,
@ -1080,22 +1273,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_encode_rust_utils(RustUtils self, SseSerializer serializer); void sse_encode_rust_utils(RustUtils self, SseSerializer serializer);
@protected @protected
void sse_encode_server_result_empty( void sse_encode_setup_profile(SetupProfile self, SseSerializer serializer);
ServerResultEmpty self,
SseSerializer serializer,
);
@protected
void sse_encode_server_result_i_64(
ServerResultI64 self,
SseSerializer serializer,
);
@protected
void sse_encode_server_result_vec_u_8(
ServerResultVecU8 self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_sql_execution_result( void sse_encode_sql_execution_result(
@ -1112,6 +1290,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_sql_value(SqlValue self, SseSerializer serializer); void sse_encode_sql_value(SqlValue self, SseSerializer serializer);
@protected
void sse_encode_theme_mode(ThemeMode self, SseSerializer serializer);
@protected
void sse_encode_twonly_safe_backup(
TwonlySafeBackup self,
SseSerializer serializer,
);
@protected @protected
void sse_encode_u_32(int self, SseSerializer serializer); void sse_encode_u_32(int self, SseSerializer serializer);
@ -1127,6 +1314,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_unit(void self, SseSerializer serializer); void sse_encode_unit(void self, SseSerializer serializer);
@protected
void sse_encode_user_config(UserConfig self, SseSerializer serializer);
@protected
void sse_encode_user_config_api(UserConfigApi self, SseSerializer serializer);
@protected @protected
void sse_encode_usize(BigInt self, SseSerializer serializer); void sse_encode_usize(BigInt self, SseSerializer serializer);
} }

357
lib/core/user_config.dart Normal file
View file

@ -0,0 +1,357 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
enum LastBackupUploadState {
none,
pending,
failed,
success,
}
class PasswordlessRecoveryConfig {
String? email;
PlatformInt64 threshold;
Uint8List? serverKeyProtection;
Uint8List? pinUnlockToken;
DateTime? lastServerHeartbeat;
DateTime? lastContactHeartbeat;
Uint8List? encryptedServerKey;
PasswordlessRecoveryConfig({
this.email,
required this.threshold,
this.serverKeyProtection,
this.pinUnlockToken,
this.lastServerHeartbeat,
this.lastContactHeartbeat,
this.encryptedServerKey,
});
@override
int get hashCode =>
email.hashCode ^
threshold.hashCode ^
serverKeyProtection.hashCode ^
pinUnlockToken.hashCode ^
lastServerHeartbeat.hashCode ^
lastContactHeartbeat.hashCode ^
encryptedServerKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PasswordlessRecoveryConfig &&
runtimeType == other.runtimeType &&
email == other.email &&
threshold == other.threshold &&
serverKeyProtection == other.serverKeyProtection &&
pinUnlockToken == other.pinUnlockToken &&
lastServerHeartbeat == other.lastServerHeartbeat &&
lastContactHeartbeat == other.lastContactHeartbeat &&
encryptedServerKey == other.encryptedServerKey;
}
enum SetupProfile {
standard,
customized,
}
enum ThemeMode {
system,
light,
dark,
}
class TwonlySafeBackup {
PlatformInt64 lastBackupSize;
LastBackupUploadState backupUploadState;
DateTime? lastBackupDone;
Uint8List backupId;
Uint8List encryptionKey;
TwonlySafeBackup({
required this.lastBackupSize,
required this.backupUploadState,
this.lastBackupDone,
required this.backupId,
required this.encryptionKey,
});
@override
int get hashCode =>
lastBackupSize.hashCode ^
backupUploadState.hashCode ^
lastBackupDone.hashCode ^
backupId.hashCode ^
encryptionKey.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TwonlySafeBackup &&
runtimeType == other.runtimeType &&
lastBackupSize == other.lastBackupSize &&
backupUploadState == other.backupUploadState &&
lastBackupDone == other.lastBackupDone &&
backupId == other.backupId &&
encryptionKey == other.encryptionKey;
}
class UserConfig {
PlatformInt64 userId;
String username;
String displayName;
String? avatarSvg;
PlatformInt64 appVersion;
PlatformInt64 avatarCounter;
bool videoStabilizationEnabled;
bool isDeveloper;
PlatformInt64 deviceId;
SetupProfile setupProfile;
String subscriptionPlan;
String? subscriptionPlanIdStore;
DateTime? lastImageSend;
PlatformInt64? todaysImageCounter;
String? lastPlanBallance;
String? additionalUserInvites;
ThemeMode themeMode;
PlatformInt64? primaryColorValue;
PlatformInt64? defaultShowTime;
bool requestedAudioPermission;
bool enableDatabaseLogging;
bool automaticallyMarkEqualMediaFilesAsOpened;
bool showNewsShortcut;
bool showShowImagePreviewWhenSending;
bool startWithCameraOpen;
List<String>? preSelectedEmojies;
Map<String, List<String>>? autoDownloadOptions;
bool storeMediaFilesInGallery;
bool autoStoreAllSendUnlimitedMediaFiles;
bool typingIndicators;
bool showRestoreFlame;
String? myBestFriendGroupId;
DateTime? signalLastSignedPreKeyUpdated;
DateTime? signalLastPqcPreKeysUploaded;
bool allowErrorTrackingViaSentry;
bool screenLockEnabled;
bool isCloudBackupEnabled;
bool isUserDiscoveryEnabled;
PlatformInt64 requiredSendImages;
int userDiscoveryThreshold;
bool userDiscoveryRequiresManualApproval;
bool userDiscoverySharePromotion;
bool userDiscoveryInitializationError;
bool askForFriendPromotions;
PlatformInt64 currentPreKeyIndexStart;
PlatformInt64 currentSignedPreKeyIndexStart;
Uint8List? lastChangeLogHash;
bool hideChangeLog;
bool hideMemoriesBackupPromo;
bool updateFcmToken;
bool canUseLoginTokenForAuth;
TwonlySafeBackup? twonlySafeBackup;
bool isBackupEnabled;
PasswordlessRecoveryConfig? passwordLessRecovery;
String? fcmToken;
String? currentSetupPage;
bool skipSetupPages;
bool hasZoomed;
UserConfig({
required this.userId,
required this.username,
required this.displayName,
this.avatarSvg,
required this.appVersion,
required this.avatarCounter,
required this.videoStabilizationEnabled,
required this.isDeveloper,
required this.deviceId,
required this.setupProfile,
required this.subscriptionPlan,
this.subscriptionPlanIdStore,
this.lastImageSend,
this.todaysImageCounter,
this.lastPlanBallance,
this.additionalUserInvites,
required this.themeMode,
this.primaryColorValue,
this.defaultShowTime,
required this.requestedAudioPermission,
required this.enableDatabaseLogging,
required this.automaticallyMarkEqualMediaFilesAsOpened,
required this.showNewsShortcut,
required this.showShowImagePreviewWhenSending,
required this.startWithCameraOpen,
this.preSelectedEmojies,
this.autoDownloadOptions,
required this.storeMediaFilesInGallery,
required this.autoStoreAllSendUnlimitedMediaFiles,
required this.typingIndicators,
required this.showRestoreFlame,
this.myBestFriendGroupId,
this.signalLastSignedPreKeyUpdated,
this.signalLastPqcPreKeysUploaded,
required this.allowErrorTrackingViaSentry,
required this.screenLockEnabled,
required this.isCloudBackupEnabled,
required this.isUserDiscoveryEnabled,
required this.requiredSendImages,
required this.userDiscoveryThreshold,
required this.userDiscoveryRequiresManualApproval,
required this.userDiscoverySharePromotion,
required this.userDiscoveryInitializationError,
required this.askForFriendPromotions,
required this.currentPreKeyIndexStart,
required this.currentSignedPreKeyIndexStart,
this.lastChangeLogHash,
required this.hideChangeLog,
required this.hideMemoriesBackupPromo,
required this.updateFcmToken,
required this.canUseLoginTokenForAuth,
this.twonlySafeBackup,
required this.isBackupEnabled,
this.passwordLessRecovery,
this.fcmToken,
this.currentSetupPage,
required this.skipSetupPages,
required this.hasZoomed,
});
@override
int get hashCode =>
userId.hashCode ^
username.hashCode ^
displayName.hashCode ^
avatarSvg.hashCode ^
appVersion.hashCode ^
avatarCounter.hashCode ^
videoStabilizationEnabled.hashCode ^
isDeveloper.hashCode ^
deviceId.hashCode ^
setupProfile.hashCode ^
subscriptionPlan.hashCode ^
subscriptionPlanIdStore.hashCode ^
lastImageSend.hashCode ^
todaysImageCounter.hashCode ^
lastPlanBallance.hashCode ^
additionalUserInvites.hashCode ^
themeMode.hashCode ^
primaryColorValue.hashCode ^
defaultShowTime.hashCode ^
requestedAudioPermission.hashCode ^
enableDatabaseLogging.hashCode ^
automaticallyMarkEqualMediaFilesAsOpened.hashCode ^
showNewsShortcut.hashCode ^
showShowImagePreviewWhenSending.hashCode ^
startWithCameraOpen.hashCode ^
preSelectedEmojies.hashCode ^
autoDownloadOptions.hashCode ^
storeMediaFilesInGallery.hashCode ^
autoStoreAllSendUnlimitedMediaFiles.hashCode ^
typingIndicators.hashCode ^
showRestoreFlame.hashCode ^
myBestFriendGroupId.hashCode ^
signalLastSignedPreKeyUpdated.hashCode ^
signalLastPqcPreKeysUploaded.hashCode ^
allowErrorTrackingViaSentry.hashCode ^
screenLockEnabled.hashCode ^
isCloudBackupEnabled.hashCode ^
isUserDiscoveryEnabled.hashCode ^
requiredSendImages.hashCode ^
userDiscoveryThreshold.hashCode ^
userDiscoveryRequiresManualApproval.hashCode ^
userDiscoverySharePromotion.hashCode ^
userDiscoveryInitializationError.hashCode ^
askForFriendPromotions.hashCode ^
currentPreKeyIndexStart.hashCode ^
currentSignedPreKeyIndexStart.hashCode ^
lastChangeLogHash.hashCode ^
hideChangeLog.hashCode ^
hideMemoriesBackupPromo.hashCode ^
updateFcmToken.hashCode ^
canUseLoginTokenForAuth.hashCode ^
twonlySafeBackup.hashCode ^
isBackupEnabled.hashCode ^
passwordLessRecovery.hashCode ^
fcmToken.hashCode ^
currentSetupPage.hashCode ^
skipSetupPages.hashCode ^
hasZoomed.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserConfig &&
runtimeType == other.runtimeType &&
userId == other.userId &&
username == other.username &&
displayName == other.displayName &&
avatarSvg == other.avatarSvg &&
appVersion == other.appVersion &&
avatarCounter == other.avatarCounter &&
videoStabilizationEnabled == other.videoStabilizationEnabled &&
isDeveloper == other.isDeveloper &&
deviceId == other.deviceId &&
setupProfile == other.setupProfile &&
subscriptionPlan == other.subscriptionPlan &&
subscriptionPlanIdStore == other.subscriptionPlanIdStore &&
lastImageSend == other.lastImageSend &&
todaysImageCounter == other.todaysImageCounter &&
lastPlanBallance == other.lastPlanBallance &&
additionalUserInvites == other.additionalUserInvites &&
themeMode == other.themeMode &&
primaryColorValue == other.primaryColorValue &&
defaultShowTime == other.defaultShowTime &&
requestedAudioPermission == other.requestedAudioPermission &&
enableDatabaseLogging == other.enableDatabaseLogging &&
automaticallyMarkEqualMediaFilesAsOpened ==
other.automaticallyMarkEqualMediaFilesAsOpened &&
showNewsShortcut == other.showNewsShortcut &&
showShowImagePreviewWhenSending ==
other.showShowImagePreviewWhenSending &&
startWithCameraOpen == other.startWithCameraOpen &&
preSelectedEmojies == other.preSelectedEmojies &&
autoDownloadOptions == other.autoDownloadOptions &&
storeMediaFilesInGallery == other.storeMediaFilesInGallery &&
autoStoreAllSendUnlimitedMediaFiles ==
other.autoStoreAllSendUnlimitedMediaFiles &&
typingIndicators == other.typingIndicators &&
showRestoreFlame == other.showRestoreFlame &&
myBestFriendGroupId == other.myBestFriendGroupId &&
signalLastSignedPreKeyUpdated ==
other.signalLastSignedPreKeyUpdated &&
signalLastPqcPreKeysUploaded == other.signalLastPqcPreKeysUploaded &&
allowErrorTrackingViaSentry == other.allowErrorTrackingViaSentry &&
screenLockEnabled == other.screenLockEnabled &&
isCloudBackupEnabled == other.isCloudBackupEnabled &&
isUserDiscoveryEnabled == other.isUserDiscoveryEnabled &&
requiredSendImages == other.requiredSendImages &&
userDiscoveryThreshold == other.userDiscoveryThreshold &&
userDiscoveryRequiresManualApproval ==
other.userDiscoveryRequiresManualApproval &&
userDiscoverySharePromotion == other.userDiscoverySharePromotion &&
userDiscoveryInitializationError ==
other.userDiscoveryInitializationError &&
askForFriendPromotions == other.askForFriendPromotions &&
currentPreKeyIndexStart == other.currentPreKeyIndexStart &&
currentSignedPreKeyIndexStart ==
other.currentSignedPreKeyIndexStart &&
lastChangeLogHash == other.lastChangeLogHash &&
hideChangeLog == other.hideChangeLog &&
hideMemoriesBackupPromo == other.hideMemoriesBackupPromo &&
updateFcmToken == other.updateFcmToken &&
canUseLoginTokenForAuth == other.canUseLoginTokenForAuth &&
twonlySafeBackup == other.twonlySafeBackup &&
isBackupEnabled == other.isBackupEnabled &&
passwordLessRecovery == other.passwordLessRecovery &&
fcmToken == other.fcmToken &&
currentSetupPage == other.currentSetupPage &&
skipSetupPages == other.skipSetupPages &&
hasZoomed == other.hasZoomed;
}

View file

@ -5,6 +5,10 @@ import 'package:twonly/src/services/api/api.service.dart';
import 'package:twonly/src/services/news.service.dart'; import 'package:twonly/src/services/news.service.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
export 'package:twonly/core/bridge/api.dart';
export 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
export 'package:twonly/src/services/api/rust_api_result.dart';
final GetIt locator = GetIt.instance; final GetIt locator = GetIt.instance;
void setupLocator() { void setupLocator() {

View file

@ -29,7 +29,6 @@ import 'package:twonly/src/services/memories/memories.service.dart';
import 'package:twonly/src/services/migrations.service.dart'; import 'package:twonly/src/services/migrations.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -204,8 +203,6 @@ Future<void> postStartupTasks() async {
}), }),
); );
unawaited(UserDiscoveryService.verifyInitializationOnStartup());
await Future.delayed(const Duration(seconds: 10)); await Future.delayed(const Duration(seconds: 10));
unawaited(initializeBackgroundTaskManager()); unawaited(initializeBackgroundTaskManager());
// 3. Delayed tasks (Wait for app to settle) // 3. Delayed tasks (Wait for app to settle)

View file

@ -1,65 +1,25 @@
import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/core/bridge/callbacks.dart'; import 'package:twonly/core/bridge/callbacks.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/callbacks/legacy_signal.callbacks.dart'; import 'package:twonly/src/callbacks/legacy_signal.callbacks.dart';
import 'package:twonly/src/callbacks/logging.callbacks.dart'; import 'package:twonly/src/callbacks/logging.callbacks.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'
as push_pb;
import 'package:twonly/src/services/api/client2client/errors.c2c.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/key_verification.service.dart'; import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/avatars.dart'; import 'package:twonly/src/utils/avatars.dart';
Future<Uint8List?> _apiCreatePushData(
int contactId,
String? messageId,
Uint8List plaintext,
int messageType,
) async {
final notification = messageType == pb.Message_Type.TEST_NOTIFICATION.value
? push_pb.PushNotification(kind: push_pb.PushKind.TEST_NOTIFICATION)
: await getPushNotificationFromEncryptedContent(
contactId,
messageId,
pb.EncryptedContent.fromBuffer(plaintext),
);
if (notification == null) return null;
return encryptPushNotification(contactId, notification);
}
Future<void> _apiMediaAction( Future<void> _apiMediaAction(
String kind, String kind,
String mediaId, String mediaId,
int contactId, int contactId,
String messageId, String messageId,
) async { ) async {
if (kind == 'response') {
await handleMediaRelatedResponseFromReceiver(messageId);
return;
}
if (kind == 'delete') {
final media = await MediaFileService.fromMediaId(mediaId);
media?.fullMediaRemoval();
return;
}
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId); final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
if (media == null) return; if (media == null) return;
switch (kind) { switch (kind) {
case 'download':
await startDownloadMedia(media, false);
case 'stored': case 'stored':
await MediaFileService(media).storeMediaFile(); await MediaFileService(media).storeMediaFile();
case 'reupload': case 'reupload':
@ -67,26 +27,6 @@ Future<void> _apiMediaAction(
} }
} }
Future<void> _apiGroupStateRefresh(String groupId, bool created) async {
if (created) {
await fetchGroupStatesForUnjoinedGroups();
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group?.myGroupPrivateKey == null) return;
final key = IdentityKeyPair.fromSerialized(group!.myGroupPrivateKey!);
await sendCipherTextToGroup(
groupId,
pb.EncryptedContent(
groupJoin: pb.EncryptedContent_GroupJoin(
groupPublicKey: key.getPublicKey().serialize(),
),
),
);
return;
}
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group != null) await fetchGroupState(group);
}
Future<void> initFlutterCallbacksForRust() async { Future<void> initFlutterCallbacksForRust() async {
await initFlutterCallbacks( await initFlutterCallbacks(
callbackId: isolateCallbackId, callbackId: isolateCallbackId,
@ -95,31 +35,15 @@ Future<void> initFlutterCallbacksForRust() async {
legacySignalEncrypt: LegacySignalCallbacks.encrypt, legacySignalEncrypt: LegacySignalCallbacks.encrypt,
legacySignalGeneratePrekeys: LegacySignalCallbacks.generatePrekeys, legacySignalGeneratePrekeys: LegacySignalCallbacks.generatePrekeys,
apiResyncSignalSession: handleSessionResync, apiResyncSignalSession: handleSessionResync,
apiPushKeyRequested: (contactId) =>
setupNotificationWithUsers(forceContact: contactId),
apiGroupMembershipError: (contactId, groupId, relatedReceiptId) =>
handleErrorMessage(
contactId,
pb.EncryptedContent_ErrorMessages(
type: pb
.EncryptedContent_ErrorMessages_Type
.GROUP_NOT_FOUND_OR_NOT_A_MEMBER,
relatedReceiptId: relatedReceiptId,
),
relatedReceiptId,
groupId: groupId,
),
apiMediaAction: _apiMediaAction, apiMediaAction: _apiMediaAction,
apiVerificationProof: KeyVerificationService.handleVerificationProof, apiVerificationProof: KeyVerificationService.handleVerificationProof,
apiCreatePushData: _apiCreatePushData,
apiCreatePushAvatars: (contactId) => apiCreatePushAvatars: (contactId) =>
createPushAvatars(forceForUserId: contactId), createPushAvatars(forceForUserId: contactId),
apiRecoveryChanged: PasswordlessRecoveryService.performHeartbeat,
apiMediaReceived: (groupId, timestamp) => incFlameCounter( apiMediaReceived: (groupId, timestamp) => incFlameCounter(
groupId, groupId,
true, true,
DateTime.fromMillisecondsSinceEpoch(timestamp), DateTime.fromMillisecondsSinceEpoch(timestamp),
), ),
apiGroupStateRefresh: _apiGroupStateRefresh, apiUserConfigChanged: UserService.handleRustUserConfigChanged,
); );
} }

View file

@ -2,7 +2,6 @@ import 'package:drift/drift.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
part 'contacts.dao.g.dart'; part 'contacts.dao.g.dart';
@ -86,7 +85,6 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
updatedValues.username.present) { updatedValues.username.present) {
final contact = await getContactByUserId(userId).getSingleOrNull(); final contact = await getContactByUserId(userId).getSingleOrNull();
if (contact != null) { if (contact != null) {
await updatePushUser(contact);
final group = await twonlyDB.groupsDao.getDirectChat(userId); final group = await twonlyDB.groupsDao.getDirectChat(userId);
if (group != null) { if (group != null) {
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(

View file

@ -1,236 +0,0 @@
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:twonly/src/services/profile.service.dart';
part 'userdata.model.g.dart';
@JsonSerializable()
class UserData {
UserData({
required this.userId,
required this.username,
required this.displayName,
required this.subscriptionPlan,
required this.currentSetupPage,
required this.appVersion,
});
factory UserData.fromJson(Map<String, dynamic> json) =>
_$UserDataFromJson(json);
final int userId;
// -- USER PROFILE --
String username;
String displayName;
String? avatarSvg;
@JsonKey(defaultValue: 0)
int appVersion = 0;
@JsonKey(defaultValue: 0)
int avatarCounter = 0;
@JsonKey(defaultValue: false)
bool isDeveloper = false;
@JsonKey(defaultValue: 0)
int deviceId = 0;
@JsonKey(defaultValue: SetupProfile.standard)
SetupProfile setupProfile = SetupProfile.standard;
// --- SUBSCRIPTION DTA ---
@JsonKey(defaultValue: 'Free')
String subscriptionPlan;
String? subscriptionPlanIdStore;
DateTime? lastImageSend;
int? todaysImageCounter;
String? lastPlanBallance;
String? additionalUserInvites;
// --- SETTINGS ---
@JsonKey(defaultValue: ThemeMode.system)
ThemeMode themeMode = ThemeMode.system;
int? primaryColorValue;
Color get primaryColor => primaryColorValue != null
? Color(primaryColorValue!)
: const Color(0xFF57CC99);
int? defaultShowTime;
@JsonKey(defaultValue: false)
bool requestedAudioPermission = false;
@JsonKey(defaultValue: false)
bool enableDatabaseLogging = false;
@JsonKey(defaultValue: false)
bool automaticallyMarkEqualMediaFilesAsOpened = false;
@JsonKey(defaultValue: true)
bool videoStabilizationEnabled = true;
@JsonKey(defaultValue: true)
bool showNewsShortcut = true;
@JsonKey(defaultValue: false)
bool showShowImagePreviewWhenSending = false;
@JsonKey(defaultValue: true)
bool startWithCameraOpen = true;
List<String>? preSelectedEmojies;
Map<String, List<String>>? autoDownloadOptions;
@JsonKey(defaultValue: true)
bool storeMediaFilesInGallery = true;
@JsonKey(defaultValue: false)
bool autoStoreAllSendUnlimitedMediaFiles = false;
@JsonKey(defaultValue: true)
bool typingIndicators = true;
@JsonKey(defaultValue: true)
bool showRestoreFlame = true;
String? myBestFriendGroupId;
DateTime? signalLastSignedPreKeyUpdated;
DateTime? signalLastPqcPreKeysUploaded;
@JsonKey(defaultValue: false)
bool allowErrorTrackingViaSentry = false;
@JsonKey(defaultValue: false)
bool screenLockEnabled = false;
@JsonKey(defaultValue: false)
bool isCloudBackupEnabled = false;
// > User Discovery Configurations
@JsonKey(defaultValue: false)
bool isUserDiscoveryEnabled = false;
@JsonKey(defaultValue: 4)
int requiredSendImages = 4;
@JsonKey(defaultValue: 3)
int userDiscoveryThreshold = 3;
@JsonKey(defaultValue: false)
bool userDiscoveryRequiresManualApproval = false;
@JsonKey(defaultValue: true)
bool userDiscoverySharePromotion = true;
@JsonKey(defaultValue: false)
bool userDiscoveryInitializationError = false;
// -- Custom DATA --
@JsonKey(defaultValue: true)
bool askForFriendPromotions = true;
@JsonKey(defaultValue: 100_000)
int currentPreKeyIndexStart = 100_000;
@JsonKey(defaultValue: 100_000)
int currentSignedPreKeyIndexStart = 100_000;
List<int>? lastChangeLogHash;
@JsonKey(defaultValue: true)
bool hideChangeLog = true;
@JsonKey(defaultValue: false)
bool hideMemoriesBackupPromo = false;
@JsonKey(defaultValue: true)
bool updateFCMToken = true;
@JsonKey(defaultValue: true)
bool canUseLoginTokenForAuth = true;
// --- BACKUP ---
@Deprecated('Use the secure storage in rust')
TwonlySafeBackup? twonlySafeBackup;
@JsonKey(defaultValue: false)
bool isBackupEnabled = false;
PasswordLessRecovery? passwordLessRecovery;
// Used for push notifcation via FCM.
String? fcmToken;
String? currentSetupPage;
@JsonKey(defaultValue: false)
bool skipSetupPages = false;
@JsonKey(defaultValue: false)
bool hasZoomed = false;
Map<String, dynamic> toJson() => _$UserDataToJson(this);
}
enum LastBackupUploadState { none, pending, failed, success }
@JsonSerializable()
class TwonlySafeBackup {
TwonlySafeBackup({
required this.backupId,
required this.encryptionKey,
});
factory TwonlySafeBackup.fromJson(Map<String, dynamic> json) =>
_$TwonlySafeBackupFromJson(json);
@JsonKey(defaultValue: 0)
int lastBackupSize = 0;
@JsonKey(defaultValue: LastBackupUploadState.none)
LastBackupUploadState backupUploadState = LastBackupUploadState.none;
DateTime? lastBackupDone;
List<int> backupId;
List<int> encryptionKey;
Map<String, dynamic> toJson() => _$TwonlySafeBackupToJson(this);
}
@JsonSerializable()
class PasswordLessRecovery {
PasswordLessRecovery(this.threshold);
factory PasswordLessRecovery.fromJson(Map<String, dynamic> json) =>
_$PasswordLessRecoveryFromJson(json);
// Only stored, so the user can see his deposit email address...
String? email;
// Data shared with trusted friends
@JsonKey(defaultValue: 2)
int threshold;
// Used to derive the key from the email/pin
List<int>? serverKeyProtection;
List<int>? pinUnlockToken;
// --->
// Checking with the server that the server data is valid and not delted throug the pin protection for example.
DateTime? lastServerHeartbeat;
DateTime? lastContactHeartbeat;
List<int>? encryptedServerKey;
Map<String, dynamic> toJson() => _$PasswordLessRecoveryToJson(this);
}

View file

@ -1,261 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'userdata.model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserData _$UserDataFromJson(Map<String, dynamic> json) =>
UserData(
userId: (json['userId'] as num).toInt(),
username: json['username'] as String,
displayName: json['displayName'] as String,
subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Free',
currentSetupPage: json['currentSetupPage'] as String?,
appVersion: (json['appVersion'] as num?)?.toInt() ?? 0,
)
..avatarSvg = json['avatarSvg'] as String?
..avatarCounter = (json['avatarCounter'] as num?)?.toInt() ?? 0
..isDeveloper = json['isDeveloper'] as bool? ?? false
..deviceId = (json['deviceId'] as num?)?.toInt() ?? 0
..setupProfile =
$enumDecodeNullable(_$SetupProfileEnumMap, json['setupProfile']) ??
SetupProfile.standard
..subscriptionPlanIdStore = json['subscriptionPlanIdStore'] as String?
..lastImageSend = json['lastImageSend'] == null
? null
: DateTime.parse(json['lastImageSend'] as String)
..todaysImageCounter = (json['todaysImageCounter'] as num?)?.toInt()
..lastPlanBallance = json['lastPlanBallance'] as String?
..additionalUserInvites = json['additionalUserInvites'] as String?
..themeMode =
$enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system
..primaryColorValue = (json['primaryColorValue'] as num?)?.toInt()
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false
..enableDatabaseLogging = json['enableDatabaseLogging'] as bool? ?? false
..automaticallyMarkEqualMediaFilesAsOpened =
json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false
..videoStabilizationEnabled =
json['videoStabilizationEnabled'] as bool? ?? true
..showNewsShortcut = json['showNewsShortcut'] as bool? ?? true
..showShowImagePreviewWhenSending =
json['showShowImagePreviewWhenSending'] as bool? ?? false
..startWithCameraOpen = json['startWithCameraOpen'] as bool? ?? true
..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?)
?.map((e) => e as String)
.toList()
..autoDownloadOptions =
(json['autoDownloadOptions'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(
k,
(e as List<dynamic>).map((e) => e as String).toList(),
),
)
..storeMediaFilesInGallery =
json['storeMediaFilesInGallery'] as bool? ?? true
..autoStoreAllSendUnlimitedMediaFiles =
json['autoStoreAllSendUnlimitedMediaFiles'] as bool? ?? false
..typingIndicators = json['typingIndicators'] as bool? ?? true
..showRestoreFlame = json['showRestoreFlame'] as bool? ?? true
..myBestFriendGroupId = json['myBestFriendGroupId'] as String?
..signalLastSignedPreKeyUpdated =
json['signalLastSignedPreKeyUpdated'] == null
? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..signalLastPqcPreKeysUploaded =
json['signalLastPqcPreKeysUploaded'] == null
? null
: DateTime.parse(json['signalLastPqcPreKeysUploaded'] as String)
..allowErrorTrackingViaSentry =
json['allowErrorTrackingViaSentry'] as bool? ?? false
..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false
..isCloudBackupEnabled = json['isCloudBackupEnabled'] as bool? ?? false
..isUserDiscoveryEnabled =
json['isUserDiscoveryEnabled'] as bool? ?? false
..requiredSendImages = (json['requiredSendImages'] as num?)?.toInt() ?? 4
..userDiscoveryThreshold =
(json['userDiscoveryThreshold'] as num?)?.toInt() ?? 3
..userDiscoveryRequiresManualApproval =
json['userDiscoveryRequiresManualApproval'] as bool? ?? false
..userDiscoverySharePromotion =
json['userDiscoverySharePromotion'] as bool? ?? true
..userDiscoveryInitializationError =
json['userDiscoveryInitializationError'] as bool? ?? false
..askForFriendPromotions = json['askForFriendPromotions'] as bool? ?? true
..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart =
(json['currentSignedPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..lastChangeLogHash = (json['lastChangeLogHash'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..hideChangeLog = json['hideChangeLog'] as bool? ?? true
..hideMemoriesBackupPromo =
json['hideMemoriesBackupPromo'] as bool? ?? false
..updateFCMToken = json['updateFCMToken'] as bool? ?? true
..canUseLoginTokenForAuth =
json['canUseLoginTokenForAuth'] as bool? ?? true
..twonlySafeBackup = json['twonlySafeBackup'] == null
? null
: TwonlySafeBackup.fromJson(
json['twonlySafeBackup'] as Map<String, dynamic>,
)
..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false
..passwordLessRecovery = json['passwordLessRecovery'] == null
? null
: PasswordLessRecovery.fromJson(
json['passwordLessRecovery'] as Map<String, dynamic>,
)
..fcmToken = json['fcmToken'] as String?
..skipSetupPages = json['skipSetupPages'] as bool? ?? false
..hasZoomed = json['hasZoomed'] as bool? ?? false;
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'userId': instance.userId,
'username': instance.username,
'displayName': instance.displayName,
'avatarSvg': instance.avatarSvg,
'appVersion': instance.appVersion,
'avatarCounter': instance.avatarCounter,
'isDeveloper': instance.isDeveloper,
'deviceId': instance.deviceId,
'setupProfile': _$SetupProfileEnumMap[instance.setupProfile]!,
'subscriptionPlan': instance.subscriptionPlan,
'subscriptionPlanIdStore': instance.subscriptionPlanIdStore,
'lastImageSend': instance.lastImageSend?.toIso8601String(),
'todaysImageCounter': instance.todaysImageCounter,
'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'primaryColorValue': instance.primaryColorValue,
'defaultShowTime': instance.defaultShowTime,
'requestedAudioPermission': instance.requestedAudioPermission,
'enableDatabaseLogging': instance.enableDatabaseLogging,
'automaticallyMarkEqualMediaFilesAsOpened':
instance.automaticallyMarkEqualMediaFilesAsOpened,
'videoStabilizationEnabled': instance.videoStabilizationEnabled,
'showNewsShortcut': instance.showNewsShortcut,
'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending,
'startWithCameraOpen': instance.startWithCameraOpen,
'preSelectedEmojies': instance.preSelectedEmojies,
'autoDownloadOptions': instance.autoDownloadOptions,
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
'autoStoreAllSendUnlimitedMediaFiles':
instance.autoStoreAllSendUnlimitedMediaFiles,
'typingIndicators': instance.typingIndicators,
'showRestoreFlame': instance.showRestoreFlame,
'myBestFriendGroupId': instance.myBestFriendGroupId,
'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated
?.toIso8601String(),
'signalLastPqcPreKeysUploaded': instance.signalLastPqcPreKeysUploaded
?.toIso8601String(),
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
'screenLockEnabled': instance.screenLockEnabled,
'isCloudBackupEnabled': instance.isCloudBackupEnabled,
'isUserDiscoveryEnabled': instance.isUserDiscoveryEnabled,
'requiredSendImages': instance.requiredSendImages,
'userDiscoveryThreshold': instance.userDiscoveryThreshold,
'userDiscoveryRequiresManualApproval':
instance.userDiscoveryRequiresManualApproval,
'userDiscoverySharePromotion': instance.userDiscoverySharePromotion,
'userDiscoveryInitializationError': instance.userDiscoveryInitializationError,
'askForFriendPromotions': instance.askForFriendPromotions,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash,
'hideChangeLog': instance.hideChangeLog,
'hideMemoriesBackupPromo': instance.hideMemoriesBackupPromo,
'updateFCMToken': instance.updateFCMToken,
'canUseLoginTokenForAuth': instance.canUseLoginTokenForAuth,
'twonlySafeBackup': instance.twonlySafeBackup,
'isBackupEnabled': instance.isBackupEnabled,
'passwordLessRecovery': instance.passwordLessRecovery,
'fcmToken': instance.fcmToken,
'currentSetupPage': instance.currentSetupPage,
'skipSetupPages': instance.skipSetupPages,
'hasZoomed': instance.hasZoomed,
};
const _$SetupProfileEnumMap = {
SetupProfile.standard: 'standard',
SetupProfile.customized: 'customized',
};
const _$ThemeModeEnumMap = {
ThemeMode.system: 'system',
ThemeMode.light: 'light',
ThemeMode.dark: 'dark',
};
TwonlySafeBackup _$TwonlySafeBackupFromJson(Map<String, dynamic> json) =>
TwonlySafeBackup(
backupId: (json['backupId'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
encryptionKey: (json['encryptionKey'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
)
..lastBackupSize = (json['lastBackupSize'] as num?)?.toInt() ?? 0
..backupUploadState =
$enumDecodeNullable(
_$LastBackupUploadStateEnumMap,
json['backupUploadState'],
) ??
LastBackupUploadState.none
..lastBackupDone = json['lastBackupDone'] == null
? null
: DateTime.parse(json['lastBackupDone'] as String);
Map<String, dynamic> _$TwonlySafeBackupToJson(TwonlySafeBackup instance) =>
<String, dynamic>{
'lastBackupSize': instance.lastBackupSize,
'backupUploadState':
_$LastBackupUploadStateEnumMap[instance.backupUploadState]!,
'lastBackupDone': instance.lastBackupDone?.toIso8601String(),
'backupId': instance.backupId,
'encryptionKey': instance.encryptionKey,
};
const _$LastBackupUploadStateEnumMap = {
LastBackupUploadState.none: 'none',
LastBackupUploadState.pending: 'pending',
LastBackupUploadState.failed: 'failed',
LastBackupUploadState.success: 'success',
};
PasswordLessRecovery _$PasswordLessRecoveryFromJson(
Map<String, dynamic> json,
) => PasswordLessRecovery((json['threshold'] as num?)?.toInt() ?? 2)
..email = json['email'] as String?
..serverKeyProtection = (json['serverKeyProtection'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..pinUnlockToken = (json['pinUnlockToken'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..lastServerHeartbeat = json['lastServerHeartbeat'] == null
? null
: DateTime.parse(json['lastServerHeartbeat'] as String)
..lastContactHeartbeat = json['lastContactHeartbeat'] == null
? null
: DateTime.parse(json['lastContactHeartbeat'] as String)
..encryptedServerKey = (json['encryptedServerKey'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList();
Map<String, dynamic> _$PasswordLessRecoveryToJson(
PasswordLessRecovery instance,
) => <String, dynamic>{
'email': instance.email,
'threshold': instance.threshold,
'serverKeyProtection': instance.serverKeyProtection,
'pinUnlockToken': instance.pinUnlockToken,
'lastServerHeartbeat': instance.lastServerHeartbeat?.toIso8601String(),
'lastContactHeartbeat': instance.lastContactHeartbeat?.toIso8601String(),
'encryptedServerKey': instance.encryptedServerKey,
};

View file

@ -1618,7 +1618,7 @@ class Response_UserData extends $pb.GeneratedMessage {
create()..mergeFromJson(json, registry); create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.UserData', _omitMessageNames ? '' : 'Response.UserConfig',
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create) createEmptyInstance: create)

View file

@ -564,7 +564,7 @@ const Response_PqcBundle$json = {
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response_UserData$json = { const Response_UserData$json = {
'1': 'UserData', '1': 'UserConfig',
'2': [ '2': [
{'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'}, {'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'},
{ {
@ -868,7 +868,7 @@ const Response_Ok$json = {
'3': 5, '3': 5,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.server_to_client.Response.UserData', '6': '.server_to_client.Response.UserConfig',
'9': 0, '9': 0,
'10': 'userdata' '10': 'userdata'
}, },

View file

@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/subscription.keys.dart'; import 'package:twonly/src/constants/subscription.keys.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/purchasable_product.model.dart'; import 'package:twonly/src/model/purchasable_product.model.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
@ -107,7 +106,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
Log.info( Log.info(
'Force Ipa check was not stopped. Requesting forced check...', 'Force Ipa check was not stopped. Requesting forced check...',
); );
await apiService.forceIpaCheck(); await RustApi.forceIpaCheck();
}); });
} }
@ -181,10 +180,13 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin {
Log.info(purchaseDetails.productID); Log.info(purchaseDetails.productID);
Log.info(purchaseDetails.verificationData.source); Log.info(purchaseDetails.verificationData.source);
} }
final res = await apiService.ipaPurchase( final res = await rustApiResult(
purchaseDetails.productID, RustApi.ipaPurchase(
purchaseDetails.verificationData.source, productId: purchaseDetails.productID,
purchaseDetails.verificationData.serverVerificationData, source: purchaseDetails.verificationData.source,
verificationData:
purchaseDetails.verificationData.serverVerificationData,
),
); );
// plan is updated in the apiProvider, as the server updates its states and responses with // plan is updated in the apiProvider, as the server updates its states and responses with
// an ok authenticated which is processed in the apiProvider... // an ok authenticated which is processed in the apiProvider...

View file

@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/core/user_config.dart' as rust_config;
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/themes/light.dart';
@ -13,8 +14,15 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
void loadSettings() { void loadSettings() {
if (userService.isUserCreated) { if (userService.isUserCreated) {
_themeMode = userService.currentUser.themeMode; _themeMode = switch (userService.currentUser.themeMode) {
_primaryColor = userService.currentUser.primaryColor; rust_config.ThemeMode.system => ThemeMode.system,
rust_config.ThemeMode.light => ThemeMode.light,
rust_config.ThemeMode.dark => ThemeMode.dark,
};
final primaryColorValue = userService.currentUser.primaryColorValue;
_primaryColor = primaryColorValue == null
? defaultPrimaryColor
: Color(primaryColorValue);
notifyListeners(); notifyListeners();
} else { } else {
_themeMode = ThemeMode.system; _themeMode = ThemeMode.system;
@ -31,7 +39,13 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
notifyListeners(); notifyListeners();
await UserService.update((u) => u.themeMode = newThemeMode); await UserService.update(
(u) => u.themeMode = switch (newThemeMode) {
ThemeMode.system => rust_config.ThemeMode.system,
ThemeMode.light => rust_config.ThemeMode.light,
ThemeMode.dark => rust_config.ThemeMode.dark,
},
);
} }
Future<void> updatePrimaryColor(Color newColor) async { Future<void> updatePrimaryColor(Color newColor) async {

File diff suppressed because it is too large Load diff

View file

@ -1,75 +0,0 @@
import 'package:clock/clock.dart' show clock;
import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'
as pb_data;
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleAdditionalDataMessage(
int fromUserId,
String groupId,
EncryptedContent_AdditionalDataMessage message,
String receiptId,
) async {
Log.info(
'[$receiptId] Got a additional data message: ${message.senderMessageId} from $groupId',
);
// Prevent message overwrite: reject if a message with this ID already
// exists from a different sender.
final existing = await twonlyDB.messagesDao
.getMessageById(message.senderMessageId)
.getSingleOrNull();
if (existing != null && existing.senderId != fromUserId) {
Log.warn(
'[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
);
return;
}
try {
final additionalData = pb_data.AdditionalMessageData.fromBuffer(
message.additionalMessageData,
);
if (additionalData.type == pb_data.AdditionalMessageData_Type.CONTACTS) {
for (final sharedContact in additionalData.contacts) {
await KeyVerificationService.verifySharedContact(
contactId: sharedContact.userId.toInt(),
sharedPublicIdentityKey: sharedContact.publicIdentityKey,
senderId: fromUserId,
);
}
}
} catch (e) {
Log.error(
'Failed to parse additional message data or verify shared contacts: $e',
);
}
final msg = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
messageId: Value(message.senderMessageId),
senderId: Value(fromUserId),
groupId: Value(groupId),
type: Value(message.type),
additionalMessageData: Value(
Uint8List.fromList(message.additionalMessageData),
),
createdAt: Value(fromTimestamp(message.timestamp)),
ackByServer: Value(clock.now()),
),
);
await twonlyDB.groupsDao.increaseLastMessageExchange(
groupId,
fromTimestamp(message.timestamp),
);
if (msg != null) {
Log.info(
'[$receiptId] Inserted a new text message with ID: ${msg.messageId}',
);
}
}

View file

@ -1,249 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart' hide Message;
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/avatars.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
Future<bool> handleNewContactRequest(int fromUserId) async {
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact != null) {
// Either the contact has accepted the fromUserId already: Then just blindly accept the request.
// Or the user has also requested fromUserId. This means that both user have requested each other (while been
// offline for example): In this case the contact can also be accepted blindly.
if (contact.accepted || (!contact.requested && !contact.deletedByUser)) {
if (!contact.accepted) {
// User has also requested the fromUserId, so mark the user as accepted.
await handleContactAccept(fromUserId);
}
await sendCipherText(
contact.userId,
EncryptedContent(
contactRequest: EncryptedContent_ContactRequest(
type: EncryptedContent_ContactRequest_Type.ACCEPT,
),
),
blocking: false,
);
return true;
}
}
// Request the username by the server so an attacker can not
// forge the displayed username in the contact request
final user = await apiService.getUserById(fromUserId);
if (user == null) {
return false;
}
await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion(
username: Value(utf8.decode(user.username)),
userId: Value(fromUserId),
requested: const Value(true),
deletedByUser: const Value(false),
),
);
await setupNotificationWithUsers();
return true;
}
Future<void> handleContactAccept(int fromUserId) async {
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact == null) return;
if (contact.requested || contact.deletedByUser) {
Log.error('User has never send an request. So ignore the Accept.');
return;
}
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(
requested: Value(false),
accepted: Value(true),
deletedByUser: Value(false),
),
);
await twonlyDB.groupsDao.createNewDirectChat(
fromUserId,
GroupsCompanion(
groupName: Value(getContactDisplayName(contact)),
),
);
}
Future<bool> handleContactRequest(
int fromUserId,
EncryptedContent_ContactRequest contactRequest,
String receiptId,
) async {
switch (contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST:
Log.info('[$receiptId] Got a contact request from $fromUserId');
return handleNewContactRequest(fromUserId);
case EncryptedContent_ContactRequest_Type.ACCEPT:
Log.info('[$receiptId] Got a contact accept from $fromUserId');
await handleContactAccept(fromUserId);
case EncryptedContent_ContactRequest_Type.REJECT:
Log.info('[$receiptId] Got a contact reject from $fromUserId');
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(false),
deletedByUser: Value(true),
),
);
}
return true;
}
Future<void> handleContactUpdate(
int fromUserId,
EncryptedContent_ContactUpdate contactUpdate,
int? senderProfileCounter,
String receiptId,
) async {
switch (contactUpdate.type) {
case EncryptedContent_ContactUpdate_Type.REQUEST:
Log.info('[$receiptId] Got a contact update request from $fromUserId');
await sendContactMyProfileData(fromUserId);
case EncryptedContent_ContactUpdate_Type.UPDATE:
Log.info('[$receiptId] Got a contact update $fromUserId');
Uint8List? avatarSvgCompressed;
if (contactUpdate.hasAvatarSvgCompressed()) {
avatarSvgCompressed = Uint8List.fromList(
contactUpdate.avatarSvgCompressed,
);
}
if (contactUpdate.hasDisplayName() &&
contactUpdate.hasUsername() &&
senderProfileCounter != null) {
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact != null) {
final sharedGroups = await twonlyDB.groupsDao.getGroupsForMember(
fromUserId,
);
if (contact.username != contactUpdate.username) {
for (final group in sharedGroups) {
await twonlyDB.groupsDao.insertGroupAction(
GroupHistoriesCompanion(
groupId: Value(group.groupId),
type: const Value(GroupActionType.updatedContactUsername),
contactId: Value(fromUserId),
oldGroupName: Value(contact.username),
newGroupName: Value(contactUpdate.username),
),
);
}
}
if (contact.displayName != contactUpdate.displayName) {
for (final group in sharedGroups) {
await twonlyDB.groupsDao.insertGroupAction(
GroupHistoriesCompanion(
groupId: Value(group.groupId),
type: const Value(GroupActionType.updatedContactDisplayName),
contactId: Value(fromUserId),
oldGroupName: Value(contact.displayName ?? contact.username),
newGroupName: Value(contactUpdate.displayName),
),
);
}
}
}
await twonlyDB.contactsDao.updateContact(
fromUserId,
ContactsCompanion(
avatarSvgCompressed: Value(
avatarSvgCompressed,
),
displayName: Value(contactUpdate.displayName),
username: Value(contactUpdate.username),
senderProfileCounter: Value(senderProfileCounter),
),
);
unawaited(createPushAvatars(forceForUserId: fromUserId));
}
}
}
Future<void> handleFlameSync(
String groupId,
EncryptedContent_FlameSync flameSync,
String receiptId,
) async {
Log.info('[$receiptId] Got a flameSync for group $groupId');
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null || group.lastFlameCounterChange == null) return;
var updates = GroupsCompanion(
alsoBestFriend: Value(flameSync.bestFriend),
);
if (isToday(group.lastFlameCounterChange!) &&
isToday(fromTimestamp(flameSync.lastFlameCounterChange)) ||
flameSync.forceUpdate) {
if (flameSync.flameCounter > group.flameCounter) {
updates = updates.copyWith(
flameCounter: Value(flameSync.flameCounter.toInt()),
);
}
if (flameSync.flameCounter > group.maxFlameCounter) {
updates = updates.copyWith(
maxFlameCounter: Value(flameSync.flameCounter.toInt()),
);
}
}
await twonlyDB.groupsDao.updateGroup(group.groupId, updates);
}
Future<int?> checkForProfileUpdate(
int fromUserId,
EncryptedContent content,
) async {
int? senderProfileCounter;
if (content.hasSenderProfileCounter()) {
senderProfileCounter = content.senderProfileCounter.toInt();
if (!content.hasContactUpdate()) {
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact != null) {
if (contact.senderProfileCounter < senderProfileCounter) {
await sendCipherText(
fromUserId,
EncryptedContent(
contactUpdate: EncryptedContent_ContactUpdate(
type: EncryptedContent_ContactUpdate_Type.REQUEST,
),
),
blocking: false,
);
}
}
}
}
return senderProfileCounter;
}

View file

@ -1,99 +0,0 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value;
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'
show IdentityKeyPair;
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/services/api/messages.api.dart'
show sendCipherText, tryToSendCompleteMessage;
import 'package:twonly/src/services/group.service.dart' show fetchGroupState;
import 'package:twonly/src/utils/log.dart';
Future<void> handleErrorMessage(
int fromUserId,
EncryptedContent_ErrorMessages error,
String receiptId, {
String? groupId,
}) async {
Log.warn('[$receiptId] Got error from $fromUserId: $error');
switch (error.type) {
case EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD:
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
fromUserId,
error.relatedReceiptId,
ReceiptsCompanion(markForRetryAfterAccepted: Value(clock.now())),
);
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(
accepted: Value(false),
requested: Value(true),
),
);
case EncryptedContent_ErrorMessages_Type.SESSION_OUT_OF_SYNC:
break; // The other user initiated a new signal session, so ignore the error in this case, as the new session works...
case EncryptedContent_ErrorMessages_Type.GROUP_NOT_FOUND_OR_NOT_A_MEMBER:
if (groupId == null) {
Log.warn(
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but groupId is null.',
);
return;
}
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null) {
Log.warn(
'[$receiptId] GROUP_NOT_FOUND_OR_NOT_A_MEMBER error received, but group $groupId is not found in database.',
);
return;
}
// Update group state from the server to ensure the user is still part of the group...
final updated = await fetchGroupState(group);
if (updated) {
final members = await twonlyDB.groupsDao.getGroupNonLeftMembers(
groupId,
);
final isStillMember = members.any(
(member) => member.contactId == fromUserId,
);
if (isStillMember) {
final keyPair = IdentityKeyPair.fromSerialized(
group.myGroupPrivateKey!,
);
await sendCipherText(
fromUserId,
EncryptedContent(
groupId: groupId,
groupCreate: EncryptedContent_GroupCreate(
stateKey: group.stateEncryptionKey,
groupPublicKey: keyPair.getPublicKey().serialize(),
),
),
);
}
final r = await twonlyDB.receiptsDao.getReceiptById(
error.relatedReceiptId,
);
if (r != null) {
await twonlyDB.receiptsDao.updateReceiptWidthUserId(
fromUserId,
error.relatedReceiptId,
ReceiptsCompanion(
markForRetry: Value(clock.now()),
retryCount: Value(r.retryCount + 1),
),
);
}
// then resend: error.relatedReceiptId
await tryToSendCompleteMessage(
receiptId: error.relatedReceiptId,
blocking: false,
);
} else {}
// ignore: no_default_cases
default:
break;
}
}

View file

@ -1,255 +0,0 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleGroupCreate(
int fromUserId,
String groupId,
EncryptedContent_GroupCreate newGroup,
String receiptId,
) async {
final user = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (user == null) {
// Only contacts can invite other contacts, so this can (via the UI) not happen.
Log.error(
'[$receiptId] User is not a contact. Aborting.',
);
return;
}
// 1. Store the new group -> e.g. store the stateKey and groupPublicKey
// 2. Call function that should fetch all jobs
// 1. This function is also called in the main function, in case the state stored on the server could not be loaded
// 2. This function will also send the GroupJoin to all members -> so they get there public key
// 3. Finished
final myGroupKey = generateIdentityKeyPair();
var group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null) {
// Group state is joinedGroup -> As the current state has not yet been downloaded.
group = await twonlyDB.groupsDao.createNewGroup(
GroupsCompanion(
groupId: Value(groupId),
stateVersionId: const Value(0),
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
myGroupPrivateKey: Value(myGroupKey.serialize()),
groupName: Value(newGroup.hasGroupName() ? newGroup.groupName : ''),
joinedGroup: const Value(false),
),
);
} else {
// In this case make a group state update and check if the fromUserId is still a admin. otherwise return with an log error message
final updated = await fetchGroupState(group);
if (!updated) {
Log.error(
'[$receiptId] Received group invite/create for $groupId, but failed to fetch group state from server.',
);
return;
}
// User was already in the group, so update leftGroup back to false
await twonlyDB.groupsDao.updateGroup(
groupId,
GroupsCompanion(
stateVersionId: const Value(0),
stateEncryptionKey: Value(Uint8List.fromList(newGroup.stateKey)),
myGroupPrivateKey: Value(myGroupKey.serialize()),
joinedGroup: const Value(false),
leftGroup: const Value(false),
deletedContent: const Value(false),
),
);
}
if (group == null) {
Log.error(
'[$receiptId] Could not create new group. Probably because the group already existed.',
);
return;
}
await twonlyDB.groupsDao.insertGroupAction(
GroupHistoriesCompanion(
groupId: Value(groupId),
contactId: Value(fromUserId),
affectedContactId: const Value(null),
type: const Value(GroupActionType.addMember),
),
);
// Load group members from the server as this is the single source of truth.
// This can be done in the background, so the WebSocket message can be ACKed.
unawaited(fetchGroupStatesForUnjoinedGroups());
await sendCipherTextToGroup(
groupId,
EncryptedContent(
groupJoin: EncryptedContent_GroupJoin(
groupPublicKey: myGroupKey.getPublicKey().serialize(),
),
),
);
}
Future<void> handleGroupUpdate(
int fromUserId,
String groupId,
EncryptedContent_GroupUpdate update,
String receiptId,
) async {
Log.info('[$receiptId] Got group update for $groupId from $fromUserId');
final actionType = groupActionTypeFromString(update.groupActionType);
if (actionType == null) {
Log.error(
'[$receiptId] Group action ${update.groupActionType} is unknown ignoring.',
);
return;
}
final group = (await twonlyDB.groupsDao.getGroup(groupId))!;
if (!group.isDirectChat) {
unawaited(fetchGroupState(group));
}
switch (actionType) {
case GroupActionType.updatedGroupName:
await twonlyDB.groupsDao.insertGroupAction(
GroupHistoriesCompanion(
groupId: Value(groupId),
type: Value(actionType),
oldGroupName: Value(group.groupName),
newGroupName: Value(update.newGroupName),
contactId: Value(fromUserId),
),
);
case GroupActionType.changeDisplayMaxTime:
await twonlyDB.groupsDao.insertGroupAction(
GroupHistoriesCompanion(
groupId: Value(groupId),
type: Value(actionType),
newDeleteMessagesAfterMilliseconds: Value(
update.newDeleteMessagesAfterMilliseconds.toInt(),
),
contactId: Value(fromUserId),
),
);
if (group.isDirectChat) {
await twonlyDB.groupsDao.updateGroup(
group.groupId,
GroupsCompanion(
deleteMessagesAfterMilliseconds: Value(
update.newDeleteMessagesAfterMilliseconds.toInt(),
),
),
);
}
case GroupActionType.removedMember:
case GroupActionType.addMember:
case GroupActionType.leftGroup:
case GroupActionType.promoteToAdmin:
case GroupActionType.demoteToMember:
int? affectedContactId = update.affectedContactId.toInt();
if (affectedContactId == userService.currentUser.userId) {
affectedContactId = null;
if (actionType == GroupActionType.removedMember) {
// Oh no, I just got removed from the group...
// This state is handle this case in the fetchGroupState....
}
}
await twonlyDB.groupsDao.insertGroupAction(
GroupHistoriesCompanion(
groupId: Value(groupId),
type: Value(actionType),
affectedContactId: Value(affectedContactId),
contactId: Value(fromUserId),
),
);
case GroupActionType.createdGroup:
case GroupActionType.updatedContactUsername:
case GroupActionType.updatedContactDisplayName:
break;
}
}
Future<bool> handleGroupJoin(
int fromUserId,
String groupId,
EncryptedContent_GroupJoin join,
String receiptId,
) async {
if (await twonlyDB.contactsDao.getContactById(fromUserId) == null) {
if (!await addNewHiddenContact(fromUserId)) {
Log.error('[$receiptId] Got group join, but could not load contact.');
// This can happen in case the group join was received before the group create.
// In this case return false, which will cause the receipt to fail and the user
// will resend this message.
return false;
}
}
await twonlyDB.groupsDao.updateMember(
groupId,
fromUserId,
GroupMembersCompanion(
groupPublicKey: Value(Uint8List.fromList(join.groupPublicKey)),
),
);
return true;
}
Future<void> handleResendGroupPublicKey(
int fromUserId,
String groupId,
EncryptedContent_GroupJoin join,
String receiptId,
) async {
final group = await twonlyDB.groupsDao.getGroup(groupId);
if (group == null || group.myGroupPrivateKey == null) return;
final keyPair = IdentityKeyPair.fromSerialized(group.myGroupPrivateKey!);
await sendCipherText(
fromUserId,
EncryptedContent(
groupId: groupId,
groupJoin: EncryptedContent_GroupJoin(
groupPublicKey: keyPair.getPublicKey().serialize(),
),
),
blocking: false,
);
}
Future<void> handleTypingIndicator(
int fromUserId,
String groupId,
EncryptedContent_TypingIndicator indicator,
String receiptId,
) async {
var lastTypeIndicator = const Value<DateTime?>.absent();
if (indicator.isTyping) {
lastTypeIndicator = Value(fromTimestamp(indicator.createdAt));
}
await twonlyDB.groupsDao.updateMember(
groupId,
fromUserId,
GroupMembersCompanion(
lastChatOpened: Value(fromTimestamp(indicator.createdAt)),
lastTypeIndicator: lastTypeIndicator,
),
);
}

View file

@ -1,270 +0,0 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
hide Message;
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
Future<bool> handleMedia(
int fromUserId,
String groupId,
EncryptedContent_Media media,
String receiptId,
) async {
Log.info(
'[$receiptId] Got a media message: ${media.senderMessageId} from $groupId with type ${media.type}',
);
late MediaType mediaType;
switch (media.type) {
case EncryptedContent_Media_Type.REUPLOAD:
final message = await twonlyDB.messagesDao
.getMessageById(media.senderMessageId)
.getSingleOrNull();
if (message == null ||
message.senderId != fromUserId ||
message.mediaId == null) {
Log.warn(
'[$receiptId] Got reupload for a message that either does not exists (${message == null}) or senderId = ${message?.senderId}',
);
return false;
}
// in case there was already a downloaded file delete it...
final mediaService = await MediaFileService.fromMediaId(message.mediaId!);
if (mediaService != null && mediaService.tempPath.existsSync()) {
mediaService.tempPath.deleteSync();
}
await twonlyDB.mediaFilesDao.updateMedia(
message.mediaId!,
MediaFilesCompanion(
downloadState: const Value(DownloadState.pending),
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
),
);
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
message.mediaId!,
);
if (mediaFile != null) {
unawaited(startDownloadMedia(mediaFile, false));
}
return true;
case EncryptedContent_Media_Type.IMAGE:
mediaType = MediaType.image;
case EncryptedContent_Media_Type.VIDEO:
mediaType = MediaType.video;
case EncryptedContent_Media_Type.GIF:
mediaType = MediaType.gif;
case EncryptedContent_Media_Type.AUDIO:
mediaType = MediaType.audio;
}
var mediaIdValue = const Value<String>.absent();
final messageTmp = await twonlyDB.messagesDao
.getMessageById(media.senderMessageId)
.getSingleOrNull();
if (messageTmp != null) {
if (messageTmp.senderId != fromUserId) {
Log.warn(
'[$receiptId] $fromUserId tried to modify the message from ${messageTmp.senderId}.',
);
return false;
}
if (messageTmp.mediaId == null) {
Log.warn(
'[$receiptId] This message already exit without a mediaId. Message is dropped.',
);
return false;
}
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
messageTmp.mediaId!,
);
if (mediaFile?.downloadState != DownloadState.reuploadRequested) {
Log.warn(
'[$receiptId] This message and media file already exit and was not requested again. Dropping it.',
);
return false;
}
if (mediaFile != null) {
// media file is reuploaded use the same mediaId
mediaIdValue = Value(mediaFile.mediaId);
}
}
int? displayLimitInMilliseconds;
if (media.hasDisplayLimitInMilliseconds()) {
if (media.displayLimitInMilliseconds.toInt() < 1000) {
displayLimitInMilliseconds =
media.displayLimitInMilliseconds.toInt() * 1000;
} else {
displayLimitInMilliseconds = media.displayLimitInMilliseconds.toInt();
}
}
MediaFile? mediaFile;
Message? message;
Log.info(
'[$receiptId] Starting transaction for media message ${media.senderMessageId}',
);
await twonlyDB.transaction(() async {
mediaFile = await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
MediaFilesCompanion(
mediaId: mediaIdValue,
downloadState: const Value(DownloadState.pending),
type: Value(mediaType),
requiresAuthentication: Value(media.requiresAuthentication),
displayLimitInMilliseconds: Value(
displayLimitInMilliseconds,
),
downloadToken: Value(Uint8List.fromList(media.downloadToken)),
encryptionKey: Value(Uint8List.fromList(media.encryptionKey)),
encryptionMac: Value(Uint8List.fromList(media.encryptionMac)),
encryptionNonce: Value(Uint8List.fromList(media.encryptionNonce)),
createdAt: Value(fromTimestamp(media.timestamp)),
),
);
if (mediaFile == null) {
Log.error('[$receiptId] Could not insert media file into database');
return false;
}
Log.info(
'[$receiptId] Inserting media message: messageId=${media.senderMessageId}, mediaId=${mediaFile!.mediaId}',
);
message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
messageId: Value(media.senderMessageId),
senderId: Value(fromUserId),
groupId: Value(groupId),
mediaId: Value(mediaFile!.mediaId),
type: Value(MessageType.media.name),
additionalMessageData: Value.absentIfNull(
media.hasAdditionalMessageData()
? Uint8List.fromList(media.additionalMessageData)
: null,
),
quotesMessageId: Value(
media.hasQuoteMessageId() ? media.quoteMessageId : null,
),
createdAt: Value(fromTimestamp(media.timestamp)),
),
);
});
Log.info(
'[$receiptId] Finished transaction for media message ${media.senderMessageId}. Success: ${message != null}',
);
if (message != null && mediaFile != null) {
await twonlyDB.groupsDao.increaseLastMessageExchange(
groupId,
fromTimestamp(media.timestamp),
);
Log.info(
'[$receiptId] Inserted a new media message with ID: ${message!.messageId}',
);
await incFlameCounter(
message!.groupId,
true,
fromTimestamp(media.timestamp),
);
unawaited(startDownloadMedia(mediaFile!, false));
return true;
} else {
if (mediaFile == null && message == null) {
Log.error(
'[$receiptId] Could not insert new message as both the message and mediaFile are empty.',
);
} else if (mediaFile == null) {
Log.error(
'[$receiptId] Could not insert new message as the mediaFile is empty.',
);
} else {
Log.error(
'[$receiptId] Could not insert new message as the message is empty.',
);
}
return false;
}
}
Future<void> handleMediaUpdate(
int fromUserId,
EncryptedContent_MediaUpdate mediaUpdate,
String receiptId,
) async {
final message = await twonlyDB.messagesDao
.getMessageById(mediaUpdate.targetMessageId)
.getSingleOrNull();
if (message == null) {
// this can happen, in case the message was already deleted.
Log.info(
'[$receiptId] Got media update to message ${mediaUpdate.targetMessageId} but message not found.',
);
return;
}
if (message.mediaId == null) {
// this can happen, in case the message was already deleted.
Log.warn(
'[$receiptId] Got media update for message ${mediaUpdate.targetMessageId} which does not have a mediaId defined.',
);
return;
}
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
message.mediaId!,
);
if (mediaFile == null) {
Log.info(
'[$receiptId] Got media file update, but media file was not found ${message.mediaId}',
);
return;
}
switch (mediaUpdate.type) {
case EncryptedContent_MediaUpdate_Type.REOPENED:
Log.info('[$receiptId] Got media file reopened ${mediaFile.mediaId}');
await twonlyDB.messagesDao.updateMessageId(
message.messageId,
const MessagesCompanion(
mediaReopened: Value(true),
),
);
case EncryptedContent_MediaUpdate_Type.STORED:
Log.info('[$receiptId] Got media file stored ${mediaFile.mediaId}');
final mediaService = MediaFileService(mediaFile);
await mediaService.storeMediaFile();
await twonlyDB.messagesDao.updateMessageId(
message.messageId,
const MessagesCompanion(
mediaStored: Value(true),
),
);
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
Log.info(
'[$receiptId] Got media file decryption error ${mediaFile.mediaId}',
);
await reuploadMediaFile(fromUserId, mediaFile, message.messageId);
}
}

View file

@ -1,82 +0,0 @@
import 'package:drift/drift.dart' show Value;
import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleMessageUpdate(
int contactId,
EncryptedContent_MessageUpdate messageUpdate,
String receiptId,
) async {
switch (messageUpdate.type) {
case EncryptedContent_MessageUpdate_Type.OPENED:
Log.info(
'[$receiptId] Opened message ${messageUpdate.multipleTargetMessageIds}',
);
try {
await twonlyDB.messagesDao.handleMessagesOpened(
Value(contactId),
messageUpdate.multipleTargetMessageIds,
fromTimestamp(messageUpdate.timestamp),
);
} catch (e) {
Log.warn('[$receiptId] Error handling messages opened: $e');
}
case EncryptedContent_MessageUpdate_Type.DELETE:
if (!await isSender(
contactId,
messageUpdate.senderMessageId,
receiptId,
)) {
return;
}
Log.info('[$receiptId] Delete message ${messageUpdate.senderMessageId}');
try {
await twonlyDB.messagesDao.handleMessageDeletion(
contactId,
messageUpdate.senderMessageId,
fromTimestamp(messageUpdate.timestamp),
);
} catch (e) {
Log.warn('[$receiptId] Error handling message deletion: $e');
}
case EncryptedContent_MessageUpdate_Type.EDIT_TEXT:
if (!await isSender(
contactId,
messageUpdate.senderMessageId,
receiptId,
)) {
return;
}
Log.info('[$receiptId] Edit message ${messageUpdate.senderMessageId}');
try {
await twonlyDB.messagesDao.handleTextEdit(
contactId,
messageUpdate.senderMessageId,
messageUpdate.text,
fromTimestamp(messageUpdate.timestamp),
);
} catch (e) {
Log.warn('[$receiptId] Error handling text edit: $e');
}
}
}
Future<bool> isSender(
int fromUserId,
String messageId,
String receiptId,
) async {
final message = await twonlyDB.messagesDao
.getMessageById(messageId)
.getSingleOrNull();
if (message == null) return false;
if (message.senderId == fromUserId) {
return true;
}
Log.error(
'[$receiptId] Contact $fromUserId tried to modify the message $messageId',
);
return false;
}

View file

@ -1,43 +0,0 @@
import 'package:fixnum/fixnum.dart';
import 'package:twonly/core/bridge/wrapper/signal.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
as client;
import 'package:twonly/src/services/signal/identity.signal.dart';
Future<client.Response> handleRequestNewPreKey() async {
final localPreKeys = await signalGetPreKeys();
final prekeysList = <client.Response_PreKey>[];
for (var i = 0; i < localPreKeys.length; i++) {
prekeysList.add(
client.Response_PreKey()
..id = Int64(localPreKeys[i].id)
..prekey = localPreKeys[i].getKeyPair().publicKey.serialize(),
);
}
final prekeys = client.Response_Prekeys(prekeys: prekeysList);
final ok = client.Response_Ok()..prekeys = prekeys;
return client.Response()..ok = ok;
}
Future<client.Response?> handleRequestNewPqcPreKey() async {
final pqcKeys = await RustSignal.generatePqcPrekeys();
if (pqcKeys.isEmpty) return null;
final prekeysList = <client.ApplicationData_PqcPreKey>[];
for (final pqcKey in pqcKeys) {
prekeysList.add(
client.ApplicationData_PqcPreKey(
eccPreKeyId: Int64(pqcKey.eccPreKeyId),
eccPreKey: pqcKey.eccPreKey,
kyberPreKeyId: Int64(pqcKey.kyberPreKeyId),
kyberPreKey: pqcKey.kyberPreKey,
kyberPreKeySignature: pqcKey.kyberPreKeySignature,
),
);
}
final prekeys = client.Response_PqcPrekeys(prekeys: prekeysList);
final ok = client.Response_Ok()..prekeysPqc = prekeys;
return client.Response()..ok = ok;
}

View file

@ -1,29 +0,0 @@
import 'dart:async';
import 'package:clock/clock.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart';
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
Future<void> handlePushKey(
int contactId,
EncryptedContent_PushKeys pushKeys,
String receiptId,
) async {
switch (pushKeys.type) {
case EncryptedContent_PushKeys_Type.REQUEST:
Log.info('[$receiptId] Got a pushkey request from $contactId');
if (lastPushKeyRequest.isBefore(
clock.now().subtract(const Duration(seconds: 60)),
)) {
lastPushKeyRequest = clock.now();
unawaited(setupNotificationWithUsers(forceContact: contactId));
}
case EncryptedContent_PushKeys_Type.UPDATE:
Log.info('[$receiptId] Got a pushkey update from $contactId');
await handleNewPushKey(contactId, pushKeys.keyId.toInt(), pushKeys.key);
}
}

View file

@ -1,30 +0,0 @@
import 'package:clock/clock.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/utils/log.dart';
Future<void> handleReaction(
int fromUserId,
String groupId,
EncryptedContent_Reaction reaction,
String receiptId,
) async {
Log.info(
'[$receiptId] Got a reaction from for ${reaction.targetMessageId} (remove=${reaction.remove})',
);
await twonlyDB.reactionsDao.updateReaction(
fromUserId,
reaction.targetMessageId,
groupId,
reaction.emoji,
reaction.remove,
);
await handleMediaRelatedResponseFromReceiver(reaction.targetMessageId);
if (!reaction.remove) {
await twonlyDB.groupsDao.increaseLastMessageExchange(groupId, clock.now());
}
}

View file

@ -1,56 +0,0 @@
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/log.dart';
Future<bool> handleTextMessage(
int fromUserId,
String groupId,
EncryptedContent_TextMessage textMessage,
String receiptId,
) async {
Log.info(
'[$receiptId] Got a text message: ${textMessage.senderMessageId} from $groupId',
);
// Prevent message overwrite: reject if a message with this ID already
// exists from a different sender.
final existing = await twonlyDB.messagesDao
.getMessageById(textMessage.senderMessageId)
.getSingleOrNull();
if (existing != null && existing.senderId != fromUserId) {
Log.warn(
'[$receiptId] $fromUserId tried to overwrite message from ${existing.senderId}. Dropping.',
);
return false;
}
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
messageId: Value(textMessage.senderMessageId),
senderId: Value(fromUserId),
groupId: Value(groupId),
content: Value(textMessage.text),
type: Value(MessageType.text.name),
quotesMessageId: Value(
textMessage.hasQuoteMessageId() ? textMessage.quoteMessageId : null,
),
createdAt: Value(fromTimestamp(textMessage.timestamp)),
ackByServer: Value(clock.now()),
),
);
await twonlyDB.groupsDao.increaseLastMessageExchange(
groupId,
fromTimestamp(textMessage.timestamp),
);
if (message != null) {
Log.info(
'[$receiptId] Inserted a new text message with ID: ${message.messageId}',
);
}
return message != null;
}

View file

@ -1,107 +0,0 @@
import 'dart:typed_data';
import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/user_discovery.service.dart';
import 'package:twonly/src/utils/log.dart';
final _requestedUpdates = <int>{};
void resetUserDiscoveryRequestUpdates() {
_requestedUpdates.clear();
}
Future<void> checkForUserDiscoveryChanges(
int fromUserId,
List<int> receivedVersion,
String receiptId,
) async {
Log.info('[$receiptId] Checking for a new user discovery version.');
final currentVersion = await UserDiscoveryService.shouldRequestNewMessages(
fromUserId,
receivedVersion,
);
if (currentVersion != null) {
if (_requestedUpdates.contains(fromUserId)) {
// Only request a new version once per app session
return;
}
Log.info(
'[$receiptId] Having old version from contact. Requesting new version.',
);
_requestedUpdates.add(fromUserId);
await sendCipherText(
fromUserId,
EncryptedContent(
userDiscoveryRequest: EncryptedContent_UserDiscoveryRequest(
currentVersion: currentVersion.toList(),
),
),
blocking: false,
);
}
}
Future<void> handleUserDiscoveryRequest(
int fromUserId,
EncryptedContent_UserDiscoveryRequest request,
String receiptId,
) async {
Log.info('[$receiptId] Got a user discovery request');
if (!userService.currentUser.isUserDiscoveryEnabled) {
Log.warn('[$receiptId] Got a user discovery request while it is disabled');
return;
}
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
if (!UserDiscoveryService.isContactAllowed(contact)) {
Log.warn(
'[$receiptId] Got a request to update user discovery, but mediaSendCounter (${contact?.mediaSendCounter}) < ${userService.currentUser.requiredSendImages} or user is excluded ${contact?.userDiscoveryExcluded}',
);
return;
}
final newMessages = await UserDiscoveryService.getNewMessages(
fromUserId,
request.currentVersion,
);
if (newMessages != null && newMessages.isNotEmpty) {
Log.info(
'[$receiptId] Sending ${newMessages.length} user discovery messages',
);
await sendCipherText(
fromUserId,
EncryptedContent(
userDiscoveryUpdate: EncryptedContent_UserDiscoveryUpdate(
messages: newMessages,
),
),
blocking: false,
);
} else {
Log.info(
'[$receiptId] Got update request, but there are no new updates for the user',
);
}
}
Future<void> handleUserDiscoveryUpdate(
int fromUserId,
EncryptedContent_UserDiscoveryUpdate update,
String receiptId,
) async {
if (!userService.currentUser.isUserDiscoveryEnabled) {
Log.warn('[$receiptId] Got a user discovery update while it is disabled');
return;
}
Log.info(
'[$receiptId] Got ${update.messages.length} user discovery messages',
);
await UserDiscoveryService.handleNewMessages(
fromUserId,
update.messages.map(Uint8List.fromList).toList(),
);
}

View file

@ -1,90 +1,28 @@
import 'dart:async';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart';
import 'package:path/path.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
/// Flutter only decides whether the user's network policy permits a download.
/// Transfer, validation, decryption, hashing, and state changes are Rust-owned.
Future<void> tryDownloadAllMediaFiles({bool force = false}) async { Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
// This is called when WebSocket is newly connected, so allow all downloads to be restarted. if (force) {
await RustApi.downloadPendingMedia();
return;
}
final mediaFiles = await twonlyDB.mediaFilesDao final mediaFiles = await twonlyDB.mediaFilesDao
.getAllMediaFilesPendingDownload(); .getAllMediaFilesPendingDownload();
for (final media in mediaFiles) {
for (final mediaFile in mediaFiles) { if (await isAllowedToDownload(media.type)) {
if (await canMediaFileBeDownloaded(mediaFile)) { await RustApi.downloadMedia(mediaId: media.mediaId);
await startDownloadMedia(mediaFile, force);
} }
} }
} }
Future<bool> canMediaFileBeDownloaded(MediaFile mediaFile) async { enum DownloadMediaTypes { video, image, audio }
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
mediaFile.mediaId,
);
// Verify that the sender of the original image / message does still exists.
// If not delete the message as it can not be downloaded from the server anymore.
if (messages.length != 1) {
if (messages.isEmpty) {
MediaFileService(mediaFile).fullMediaRemoval();
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
Log.warn(
'Media file which is in downloading status has not text message. Deleting media file. ${mediaFile.mediaId}.',
);
return false;
}
Log.warn(
'A media for download must have one original message, but it has ${messages.length}.',
);
return false;
}
if (messages.first.senderId == null) {
Log.error('A media for download must have a sender id.');
return false;
}
final contact = await twonlyDB.contactsDao.getContactById(
messages.first.senderId!,
);
if (contact == null || contact.accountDeleted) {
Log.info(
'Sender does not exists anymore. Delete media file and message.',
);
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
await twonlyDB.messagesDao.deleteMessagesById(messages.first.messageId);
return false;
}
return true;
}
enum DownloadMediaTypes {
video,
image,
audio,
}
Map<String, List<String>> defaultAutoDownloadOptions = { Map<String, List<String>> defaultAutoDownloadOptions = {
ConnectivityResult.mobile.name: [ ConnectivityResult.mobile.name: [DownloadMediaTypes.audio.name],
DownloadMediaTypes.audio.name,
],
ConnectivityResult.wifi.name: [ ConnectivityResult.wifi.name: [
DownloadMediaTypes.video.name, DownloadMediaTypes.video.name,
DownloadMediaTypes.image.name, DownloadMediaTypes.image.name,
@ -93,288 +31,40 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
}; };
Future<bool> isAllowedToDownload(MediaType type) async { Future<bool> isAllowedToDownload(MediaType type) async {
if (type == MediaType.audio) { if (type == MediaType.audio) return true;
return true; // always download audio files
}
final connectivityResult = await Connectivity().checkConnectivity(); final connectivityResult = await Connectivity().checkConnectivity();
final options = final options =
userService.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions; userService.currentUser.autoDownloadOptions ?? defaultAutoDownloadOptions;
if (connectivityResult.contains(ConnectivityResult.mobile)) { if (connectivityResult.contains(ConnectivityResult.mobile)) {
if (type == MediaType.video) { return options[ConnectivityResult.mobile.name]!.contains(
if (options[ConnectivityResult.mobile.name]!.contains( type == MediaType.video
DownloadMediaTypes.video.name, ? DownloadMediaTypes.video.name
)) { : DownloadMediaTypes.image.name,
return true; );
}
} else if (options[ConnectivityResult.mobile.name]!.contains(
DownloadMediaTypes.image.name,
)) {
return true;
}
} }
if (connectivityResult.contains(ConnectivityResult.wifi)) { if (connectivityResult.contains(ConnectivityResult.wifi)) {
if (type == MediaType.video) { return options[ConnectivityResult.wifi.name]!.contains(
if (options[ConnectivityResult.wifi.name]!.contains( type == MediaType.video
DownloadMediaTypes.video.name, ? DownloadMediaTypes.video.name
)) { : DownloadMediaTypes.image.name,
return true; );
}
} else if (options[ConnectivityResult.wifi.name]!.contains(
DownloadMediaTypes.image.name,
)) {
return true;
}
} }
return false; return false;
} }
Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
final mediaId = update.task.taskId.replaceAll('download_', '');
var failed = false;
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled ||
update.status == TaskStatus.notFound) {
failed = true;
} else if (update.status == TaskStatus.complete) {
if (update.responseStatusCode == 200) {
failed = false;
} else {
failed = true;
Log.warn(
'[$mediaId] Got invalid response status code: ${update.responseStatusCode}',
);
Log.error(
'Got invalid response status code: ${update.responseStatusCode}',
onlyIfSentryEnabled: true,
);
}
} else {
Log.info('Got ${update.status} for $mediaId');
return;
}
if (failed) {
await requestMediaReupload(mediaId);
} else {
await handleEncryptedFile(mediaId);
}
}
Mutex _protectDownload = Mutex();
Mutex _protectDecryption = Mutex();
Future<void> startDownloadMedia(MediaFile media, bool force) async { Future<void> startDownloadMedia(MediaFile media, bool force) async {
final mediaService = MediaFileService(media); if (force || await isAllowedToDownload(media.type)) {
await RustApi.downloadMedia(mediaId: media.mediaId);
if (mediaService.encryptedPath.existsSync()) {
await handleEncryptedFile(media.mediaId);
return;
}
if (!force && !await isAllowedToDownload(media.type)) {
Log.warn(
'Download blocked for ${media.mediaId} because of network state.',
);
return;
}
final isBlocked = await _protectDownload.protect<bool>(() async {
final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId);
if (msg == null || msg.downloadState != DownloadState.pending) {
return true;
}
await twonlyDB.mediaFilesDao.updateMedia(
msg.mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.downloading),
),
);
return false;
});
if (isBlocked) {
Log.info('Download for ${media.mediaId} already started.');
return;
}
if (media.downloadToken == null) {
Log.info('Download token for ${media.mediaId} not found.');
return;
}
final downloadToken = uint8ListToHex(media.downloadToken!);
final apiUrl =
'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken';
try {
final task = DownloadTask(
url: apiUrl,
taskId: 'download_${media.mediaId}',
directory: mediaService.encryptedPath.parent.path,
baseDirectory: BaseDirectory.root,
filename: basename(mediaService.encryptedPath.path),
priority: 0,
retries: 10,
);
Log.info(
'Downloading ${media.mediaId} to ${mediaService.encryptedPath}',
);
try {
await downloadFileFast(media, apiUrl, mediaService.encryptedPath);
} catch (e) {
Log.warn('Fast download failed: $e');
await FileDownloader().enqueue(task);
}
} catch (e) {
Log.error('Exception during download: $e');
} }
} }
Future<void> downloadFileFast( Future<void> requestMediaReupload(String mediaId) =>
MediaFile media, RustApi.requestMediaReupload(mediaId: mediaId);
String apiUrl,
File filePath,
) async {
final response = await http
.get(Uri.parse(apiUrl))
.timeout(const Duration(seconds: 30));
if (response.statusCode == 200) {
await filePath.writeAsBytes(response.bodyBytes);
Log.info('Fast Download successful: $filePath');
await handleEncryptedFile(media.mediaId);
return;
} else {
if (response.statusCode == 404 || response.statusCode == 403) {
Log.warn(
'Got ${response.statusCode} from server for media ID ${media.mediaId}. Requesting upload again',
);
Log.error(
'Got ${response.statusCode} from server for media ID.',
onlyIfSentryEnabled: true,
);
// Message was deleted from the server. Requesting it again from the sender to upload it again...
await requestMediaReupload(media.mediaId);
return;
}
// Will be tried again using the slow method...
throw Exception('Fast download failed with status: ${response.statusCode}');
}
}
Future<void> requestMediaReupload(String mediaId) async {
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.reuploadRequested),
),
);
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
for (final message in messages) {
if (message.openedAt != null || message.senderId == null) continue;
await sendCipherText(
message.senderId!,
EncryptedContent(
mediaUpdate: EncryptedContent_MediaUpdate(
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
targetMessageId: message.messageId,
),
),
);
}
}
Future<void> handleEncryptedFile(String mediaId) async {
await exclusiveAccess(
lockName: 'decryption-$mediaId',
mutex: _protectDecryption,
action: () async {
final mediaService = await MediaFileService.fromMediaId(mediaId);
if (mediaService == null) {
Log.warn('[$mediaId] Media file not found in database.');
return;
}
if (mediaService.mediaFile.downloadState == DownloadState.ready) {
Log.info('Decryption of $mediaId already finished.');
return;
}
if (!mediaService.encryptedPath.existsSync()) {
Log.warn(
'Encrypted media file $mediaId does not exist anymore. Decryption probably already finished.',
);
return;
}
late Uint8List encryptedBytes;
try {
encryptedBytes = await mediaService.encryptedPath.readAsBytes();
} catch (e) {
Log.error('Could not read encrypted media file: $e');
await requestMediaReupload(mediaId);
return;
}
try {
final chacha20 = FlutterChacha20.poly1305Aead();
final secretKeyData = SecretKeyData(
mediaService.mediaFile.encryptionKey!,
);
final secretBox = SecretBox(
encryptedBytes,
nonce: mediaService.mediaFile.encryptionNonce!,
mac: Mac(mediaService.mediaFile.encryptionMac!),
);
final plaintextBytes = await chacha20.decrypt(
secretBox,
secretKey: secretKeyData,
);
final rawMediaBytes = Uint8List.fromList(plaintextBytes);
await mediaService.tempPath.writeAsBytes(rawMediaBytes);
} catch (e) {
Log.error(
'Could not decrypt the media file. Requesting a new upload.',
);
await requestMediaReupload(mediaId);
return;
}
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.ready),
),
);
await mediaService.hashMediaFile();
Log.info('Decryption of $mediaId was successful');
mediaService.encryptedPath.deleteSync();
},
);
}
Future<void> makeMigrationToVersion91() async { Future<void> makeMigrationToVersion91() async {
final messages = await twonlyDB.mediaFilesDao final mediaFiles = await twonlyDB.mediaFilesDao
.getAllMediaFilesReuploadRequested(); .getAllMediaFilesReuploadRequested();
for (final message in messages) { for (final media in mediaFiles) {
await requestMediaReupload(message.mediaId); await RustApi.requestMediaReupload(mediaId: media.mediaId);
} }
} }

View file

@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/download.api.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
@ -19,10 +18,6 @@ Future<void> initFileDownloader() async {
if (update.task.taskId.contains('upload_')) { if (update.task.taskId.contains('upload_')) {
await handleUploadStatusUpdate(update); await handleUploadStatusUpdate(update);
} }
if (update.task.taskId.contains('download_')) {
await handleDownloadStatusUpdate(update);
}
if (update.task.taskId.contains('backup_')) { if (update.task.taskId.contains('backup_')) {
await BackupService.handleBackupStatusUpdate( await BackupService.handleBackupStatusUpdate(
update.task.taskId, update.task.taskId,

View file

@ -7,10 +7,6 @@ import 'package:twonly/src/database/twonly.db.dart' show Receipt;
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb; as pb;
// Compatibility adapters. All messaging state and behavior lives in Rust.
Future<void> retransmitAllMessages() =>
rust_api.RustApi.retransmitAllMessages();
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
String? receiptId, String? receiptId,
Receipt? receipt, Receipt? receipt,

View file

@ -0,0 +1,50 @@
import 'dart:typed_data';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/log.dart';
Future<Result<T, ErrorCode>> rustApiResult<T>(Future<T> request) async {
try {
return Result.success(await request);
} catch (error) {
final match = RegExp(r'API error code (\d+)').firstMatch(error.toString());
if (match != null) {
return Result.error(ErrorCode.valueOf(int.parse(match.group(1)!)));
}
Log.error('Rust API call failed', error: error);
return Result.error(ErrorCode.InternalError);
}
}
Future<T?> rustApiProtobuf<T>(
Future<Uint8List> request,
T Function(List<int>) decode,
) async {
try {
return decode(await request);
} catch (error) {
Log.error('Rust API call failed', error: error);
return null;
}
}
server.Response_UserData decodeUserData(List<int> bytes) =>
server.Response_UserData.fromBuffer(bytes);
server.Response_ProofOfWork decodeProofOfWork(List<int> bytes) =>
server.Response_ProofOfWork.fromBuffer(bytes);
server.Response_MemoriesUrl decodeMemoriesUrl(List<int> bytes) =>
server.Response_MemoriesUrl.fromBuffer(bytes);
server.Response_MemoriesUploadUrls decodeMemoriesUploadUrls(List<int> bytes) =>
server.Response_MemoriesUploadUrls.fromBuffer(bytes);
server.Response_MemoriesUsage decodeMemoriesUsage(List<int> bytes) =>
server.Response_MemoriesUsage.fromBuffer(bytes);
server.Response_PlanBallance decodePlanBalance(List<int> bytes) =>
server.Response_PlanBallance.fromBuffer(bytes);

View file

@ -1,748 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:hashlib/random.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart' hide Message;
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
as client;
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/client2client/additional_data.c2c.dart';
import 'package:twonly/src/services/api/client2client/contact.c2c.dart';
import 'package:twonly/src/services/api/client2client/errors.c2c.dart';
import 'package:twonly/src/services/api/client2client/groups.c2c.dart';
import 'package:twonly/src/services/api/client2client/media.c2c.dart';
import 'package:twonly/src/services/api/client2client/messages.c2c.dart';
import 'package:twonly/src/services/api/client2client/prekeys.c2c.dart';
import 'package:twonly/src/services/api/client2client/pushkeys.c2c.dart';
import 'package:twonly/src/services/api/client2client/reaction.c2c.dart';
import 'package:twonly/src/services/api/client2client/text_message.c2c.dart';
import 'package:twonly/src/services/api/client2client/user_discovery.c2c.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/key_verification.service.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
Future<void> handleServerMessage(server.ServerToClient msg) async {
Log.info('Processing a message from the server.');
/// Returns means, that the server can delete the message from the server.
final ok = client.Response_Ok()..none = true;
var response = client.Response()..ok = ok;
try {
if (msg.v0.hasRequestNewPreKeys()) {
response = await handleRequestNewPreKey();
} else if (msg.v0.hasRequestNewPqcPreKeys()) {
response = (await handleRequestNewPqcPreKey()) ?? response;
} else if (msg.v0.hasNewMessage()) {
Log.info('Got 1 message from the server.');
await handleClient2ClientMessage(msg.v0.newMessage);
} else if (msg.v0.hasNewMessages()) {
Log.info(
'Got ${msg.v0.newMessages.newMessages.length} messages from the server.',
);
final brokenSessionsInCurrentBatch = <int>{};
for (final newMessage in msg.v0.newMessages.newMessages) {
try {
await handleClient2ClientMessage(
newMessage,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
);
} catch (e) {
Log.error(e);
}
}
} else {
Log.error('Unknown server message: $msg');
}
} catch (e) {
Log.error(e);
}
final v0 = client.V0()
..seq = msg.v0.seq
..response = response;
final responseSent = await apiService.sendResponse(ClientToServer()..v0 = v0);
if (responseSent) {
Log.info(
'Successfully queued response for server message ${msg.v0.seq}.',
);
} else {
Log.warn('Could not send response for server message ${msg.v0.seq}.');
}
AppState.gotMessageFromServer = true;
Log.info('All messages from the server processed.');
}
DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1));
final Map<String, Mutex> _messageLocks = {};
Future<void> handleClient2ClientMessage(
NewMessage newMessage, {
Set<int>? brokenSessionsInCurrentBatch,
}) async {
final body = Uint8List.fromList(newMessage.body);
final message = Message.fromBuffer(body);
final receiptId = message.receiptId;
final mutex = _messageLocks.putIfAbsent(receiptId, Mutex.new);
if (mutex.isLocked) {
Log.info(
'[$receiptId] Skipping — already being processed by another handler',
);
return;
}
await mutex.protect(() async {
try {
await _handleClient2ClientMessage(
newMessage,
message,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
);
} finally {
_messageLocks.remove(receiptId);
}
});
}
Future<void> _handleClient2ClientMessage(
NewMessage newMessage,
Message message, {
Set<int>? brokenSessionsInCurrentBatch,
}) async {
final fromUserId = newMessage.fromUserId.toInt();
final receiptId = message.receiptId;
if (brokenSessionsInCurrentBatch?.contains(fromUserId) == true) {
// This happens when a session goes out of sync (e.g. wrong message order).
// We skip the remaining messages in the batch because each failed decryption
// attempt is extremely slow (~1.2s) and would otherwise freeze the app.
// By returning early, we skip gotReceipt() and error responses.
// The server still deletes the batch since we ACK the entire batch later.
// The sender keeps the message unacknowledged. Once they process our SESSION_OUT_OF_SYNC
// error and establish a new session, their retry logic will automatically re-encrypt
// and re-send these messages with the new keys.
Log.info(
'Skipping message from $fromUserId - session known broken in this batch',
);
return;
}
if (await twonlyDB.receiptsDao.isDuplicated(receiptId)) {
if (message.type == Message_Type.SENDER_DELIVERY_RECEIPT) {
Log.info(
'[$receiptId] Delivery receipt is a duplicate. Skipping receipt response.',
);
return;
}
const duplicateReceiptCooldown = Duration(days: 10);
final shouldResend = await twonlyDB.receiptsDao.claimDuplicateReceiptResend(
receiptId,
duplicateReceiptCooldown,
);
if (!shouldResend) {
Log.info(
'[$receiptId] Message is a duplicate. Skipping delivery receipt during cooldown.',
);
return;
}
Log.info(
'[$receiptId] Message is a duplicate and cooldown elapsed. Sending delivery receipt again.',
);
try {
final response = Message(type: Message_Type.SENDER_DELIVERY_RECEIPT);
await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion(
receiptId: Value(receiptId),
contactId: Value(fromUserId),
message: Value(response.writeToBuffer()),
contactWillSendsReceipt: const Value(false),
),
);
} catch (e) {
Log.warn('[$receiptId] Error handling duplicate receipt ACK: $e');
}
await tryToSendCompleteMessage(
receiptId: receiptId,
blocking: false,
);
return;
}
Log.info('[$receiptId] Started processing message');
switch (message.type) {
case Message_Type.SENDER_DELIVERY_RECEIPT:
Log.info('[$receiptId] Got delivery receipt!');
await twonlyDB.receiptsDao.confirmReceipt(receiptId, fromUserId);
case Message_Type.PLAINTEXT_CONTENT:
var retry = false;
if (message.hasPlaintextContent()) {
if (message.plaintextContent.hasDecryptionErrorMessage()) {
if (message.plaintextContent.decryptionErrorMessage.type ==
PlaintextContent_DecryptionErrorMessage_Type.PREKEY_UNKNOWN) {
// Get a new prekey from the server, and establish a new signal session.
await handleSessionResync(fromUserId);
}
Log.info(
'[$receiptId] Got decryption error: ${message.plaintextContent.decryptionErrorMessage.type}',
);
retry = true;
}
if (message.plaintextContent.hasRetryControlError()) {
Log.info(
'[$receiptId] Got access control error. Resending message.',
);
retry = true;
}
}
if (retry) {
final newReceiptId = uuid.v4();
await twonlyDB.receiptsDao.updateReceipt(
receiptId,
ReceiptsCompanion(
receiptId: Value(newReceiptId),
ackByServerAt: const Value(null),
),
);
Log.info(
'[$receiptId] Sending error message to the original sender with receiptId $newReceiptId.',
);
await tryToSendCompleteMessage(
receiptId: newReceiptId,
blocking: false,
);
}
case Message_Type.CIPHERTEXT:
case Message_Type.CIPHERTEXT_V2:
case Message_Type.PREKEY_BUNDLE:
if (message.hasEncryptedContent()) {
Value<String>? receiptIdDB;
final encryptedContentRaw = Uint8List.fromList(
message.encryptedContent,
);
Message? response;
final user = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (user == null) {
if (!await addNewHiddenContact(fromUserId)) {
// in case the user could not be added, send a retry error message as this error should only happen in case
// it was not possible to load the user from the server
response = Message(
receiptId: receiptId,
type: Message_Type.PLAINTEXT_CONTENT,
plaintextContent: PlaintextContent(
retryControlError: PlaintextContent_RetryErrorMessage(),
),
);
}
}
if (response == null) {
final (
encryptedContent,
plainTextContent,
) = await handleEncryptedMessageRaw(
fromUserId,
encryptedContentRaw,
message.type,
receiptId,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
);
if (plainTextContent != null) {
response = Message(
receiptId: receiptId,
type: Message_Type.PLAINTEXT_CONTENT,
plaintextContent: plainTextContent,
);
} else if (encryptedContent != null) {
response = Message(
type: Message_Type.CIPHERTEXT,
encryptedContent: encryptedContent.writeToBuffer(),
);
// Use Value.absent() for CIPHERTEXT messages so that insertReceipt generates a new UUID.
// This prevents receipt ID collisions and ensures the recipient's ACK is tracked correctly.
receiptIdDB = const Value.absent();
} else {
// Message was successful processed
}
}
response ??= Message(type: Message_Type.SENDER_DELIVERY_RECEIPT);
String? targetReceiptId;
try {
final inserted = await twonlyDB.receiptsDao.insertReceipt(
ReceiptsCompanion(
receiptId: receiptIdDB ?? Value(receiptId),
contactId: Value(fromUserId),
message: Value(response.writeToBuffer()),
contactWillSendsReceipt: const Value(false),
),
);
// Use the inserted receipt's ID because for CIPHERTEXT messages we generate a new UUID
// (receiptIdDB is Value.absent()) to avoid ID collisions and properly track individual ACKs.
targetReceiptId = inserted?.receiptId;
} catch (e) {
Log.warn('[$receiptId] Error inserting receipt: $e');
}
targetReceiptId ??= receiptIdDB?.value ?? receiptId;
await tryToSendCompleteMessage(
receiptId: targetReceiptId,
blocking: false,
);
}
case Message_Type.TEST_NOTIFICATION:
break;
}
try {
await twonlyDB.receiptsDao.gotReceipt(receiptId);
Log.info('[$receiptId] Finished processing');
} catch (e) {
Log.warn('[$receiptId] Error marking message as received: $e');
Log.error(
'Error marking message as received: $e',
onlyIfSentryEnabled: true,
);
}
}
Future<(EncryptedContent?, PlaintextContent?)> handleEncryptedMessageRaw(
int fromUserId,
Uint8List encryptedContentRaw,
Message_Type messageType,
String receiptId, {
Set<int>? brokenSessionsInCurrentBatch,
}) async {
Log.info('[$receiptId] calling signalDecryptMessage');
EncryptedContent? encryptedContent;
PlaintextContent_DecryptionErrorMessage_Type? decryptionErrorType;
if (messageType == Message_Type.CIPHERTEXT_V2) {
(encryptedContent, decryptionErrorType) = await signalDecryptMessageV2(
fromUserId,
encryptedContentRaw,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
);
} else {
(encryptedContent, decryptionErrorType) = await signalDecryptMessageV1(
fromUserId,
encryptedContentRaw,
messageType.value,
brokenSessionsInCurrentBatch: brokenSessionsInCurrentBatch,
);
}
if (encryptedContent == null) {
return (
null,
PlaintextContent(
decryptionErrorMessage: PlaintextContent_DecryptionErrorMessage(
type: decryptionErrorType ??=
PlaintextContent_DecryptionErrorMessage_Type.UNKNOWN,
),
),
);
}
Log.info('[$receiptId] Calling handleEncryptedMessage');
final result = await handleEncryptedMessage(
fromUserId,
encryptedContent,
messageType,
receiptId,
);
Log.info('[$receiptId] Finished handleEncryptedMessage');
if (result.responseCipherText == null && result.responsePlaintext == null) {
unawaited(FcmNotificationService.updateLastServerMessageTimestamp());
if (Platform.isAndroid && result.showPushNotification) {
// Message was handled without any error. Show push notification to the user for Android.
await showPushNotificationFromServerMessages(
fromUserId,
encryptedContent,
);
}
}
return (result.responseCipherText, result.responsePlaintext);
}
Future<DecryptedMessageResult> handleEncryptedMessage(
int fromUserId,
EncryptedContent content,
Message_Type messageType,
String receiptId,
) async {
// We got a valid message fromUserId, so mark all messages which where
// send to the user but not yet ACK for retransmission. All marked messages
// will be either transmitted again after a new server connection (minimum 20 seconds).
// In case the server sends the ACK before they will be deleted.
// This ensures that 1. all messages will be received by the other person and
// that they will be retransmitted in case the server deleted them as they
// where not downloaded within the 40 days
await twonlyDB.receiptsDao.markMessagesForRetry(fromUserId);
final senderProfileCounter = await checkForProfileUpdate(fromUserId, content);
if (userService.currentUser.isUserDiscoveryEnabled &&
content.hasSenderUserDiscoveryVersion()) {
await checkForUserDiscoveryChanges(
fromUserId,
content.senderUserDiscoveryVersion,
receiptId,
);
}
if (content.hasAskForFriendPromotions() && content.askForFriendPromotions) {
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
if (contact != null && contact.askForFriendPromotions == null) {
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(askForFriendPromotions: Value(true)),
);
}
}
if (content.hasContactRequest()) {
if (!await handleContactRequest(
fromUserId,
content.contactRequest,
receiptId,
)) {
return DecryptedMessageResult(
responsePlaintext: PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage(),
);
}
return const DecryptedMessageResult();
}
if (content.hasErrorMessages()) {
await handleErrorMessage(
fromUserId,
content.errorMessages,
receiptId,
groupId: content.hasGroupId() ? content.groupId : null,
);
return const DecryptedMessageResult(showPushNotification: false);
}
if (content.hasPasswordlessRecovery()) {
await PasswordlessRecoveryService.handlePasswordlessRecovery(
fromUserId,
content.passwordlessRecovery,
receiptId,
);
return const DecryptedMessageResult();
}
if (content.hasPasswordlessRecoveryHeartbeat()) {
await PasswordlessRecoveryService.handlePasswordlessRecoveryHeartbeat(
fromUserId,
content.passwordlessRecoveryHeartbeat,
receiptId,
);
return const DecryptedMessageResult();
}
if (content.hasContactUpdate()) {
await handleContactUpdate(
fromUserId,
content.contactUpdate,
senderProfileCounter,
receiptId,
);
return const DecryptedMessageResult(showPushNotification: false);
}
if (content.hasUserDiscoveryRequest()) {
await handleUserDiscoveryRequest(
fromUserId,
content.userDiscoveryRequest,
receiptId,
);
return const DecryptedMessageResult(showPushNotification: false);
}
if (content.hasUserDiscoveryUpdate()) {
await handleUserDiscoveryUpdate(
fromUserId,
content.userDiscoveryUpdate,
receiptId,
);
return const DecryptedMessageResult();
}
if (content.hasPushKeys()) {
await handlePushKey(fromUserId, content.pushKeys, receiptId);
return const DecryptedMessageResult();
}
if (content.hasMessageUpdate()) {
await handleMessageUpdate(
fromUserId,
content.messageUpdate,
receiptId,
);
return const DecryptedMessageResult();
}
if (content.hasKeyVerificationProof()) {
await KeyVerificationService.handleVerificationProof(
fromUserId,
content.keyVerificationProof.calculatedMac,
);
return const DecryptedMessageResult();
}
if (content.hasMediaUpdate()) {
await handleMediaUpdate(
fromUserId,
content.mediaUpdate,
receiptId,
);
return const DecryptedMessageResult();
}
if (!content.hasGroupId()) {
final type = _getEncryptedContentType(content);
Log.warn(
'[$receiptId] Messages should have a groupId $fromUserId. Type: $type',
);
Log.error(
'Messages should have a groupId. Type: $type',
onlyIfSentryEnabled: true,
);
return const DecryptedMessageResult();
}
if (content.hasGroupCreate()) {
await handleGroupCreate(
fromUserId,
content.groupId,
content.groupCreate,
receiptId,
);
return const DecryptedMessageResult();
}
/// Verify that the user is (still) in that group...
if (!await twonlyDB.groupsDao.isContactInGroup(fromUserId, content.groupId)) {
// Check if this is a direct chat...
if (getUUIDforDirectChat(userService.currentUser.userId, fromUserId) ==
content.groupId) {
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
Log.info(
'[$receiptId] Contact exists?: ${contact != null} Is deleted? ${contact?.deletedByUser} Accepted? (${contact?.accepted})',
);
if (contact == null || !contact.accepted || contact.deletedByUser) {
await handleNewContactRequest(fromUserId);
Log.warn(
'[$receiptId] User tries to send message to direct chat while the user does not exist!',
);
return DecryptedMessageResult(
responseCipherText: EncryptedContent(
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type
.ERROR_PROCESSING_MESSAGE_CREATED_ACCOUNT_REQUEST_INSTEAD,
relatedReceiptId: receiptId,
),
),
);
}
Log.info(
'[$receiptId] Creating new DirectChat between two users',
);
await twonlyDB.groupsDao.createNewDirectChat(
fromUserId,
GroupsCompanion(
groupName: Value(getContactDisplayName(contact)),
),
);
} else {
if (content.hasGroupJoin()) {
Log.warn(
'[$receiptId] Got group join message, but group does not exist yet, retry later. As probably the GroupCreate was not yet received.',
);
// In case the group join was received before the GroupCreate the sender should send it later again.
return DecryptedMessageResult(
responsePlaintext: PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage(),
);
}
Log.warn(
'[$receiptId] User $fromUserId tried to access group ${content.groupId}. Sending GROUP_NOT_FOUND_OR_NOT_A_MEMBER error.',
);
return DecryptedMessageResult(
responseCipherText: EncryptedContent(
groupId: content.groupId,
errorMessages: EncryptedContent_ErrorMessages(
type: EncryptedContent_ErrorMessages_Type
.GROUP_NOT_FOUND_OR_NOT_A_MEMBER,
relatedReceiptId: receiptId,
),
),
);
}
}
if (content.hasFlameSync()) {
await handleFlameSync(content.groupId, content.flameSync, receiptId);
return const DecryptedMessageResult();
}
if (content.hasGroupUpdate()) {
await handleGroupUpdate(
fromUserId,
content.groupId,
content.groupUpdate,
receiptId,
);
return const DecryptedMessageResult();
}
if (content.hasGroupJoin()) {
if (!await handleGroupJoin(
fromUserId,
content.groupId,
content.groupJoin,
receiptId,
)) {
return DecryptedMessageResult(
responsePlaintext: PlaintextContent()
..retryControlError = PlaintextContent_RetryErrorMessage(),
);
}
return const DecryptedMessageResult();
}
if (content.hasResendGroupPublicKey()) {
await handleResendGroupPublicKey(
fromUserId,
content.groupId,
content.groupJoin,
receiptId,
);
return const DecryptedMessageResult();
}
if (content.hasAdditionalDataMessage()) {
await handleAdditionalDataMessage(
fromUserId,
content.groupId,
content.additionalDataMessage,
receiptId,
);
return const DecryptedMessageResult();
}
if (content.hasTextMessage()) {
final isNewText = await handleTextMessage(
fromUserId,
content.groupId,
content.textMessage,
receiptId,
);
return DecryptedMessageResult(showPushNotification: isNewText);
}
if (content.hasReaction()) {
await handleReaction(
fromUserId,
content.groupId,
content.reaction,
receiptId,
);
return const DecryptedMessageResult();
}
if (content.hasMedia()) {
final isNewMedia = await handleMedia(
fromUserId,
content.groupId,
content.media,
receiptId,
);
return DecryptedMessageResult(showPushNotification: isNewMedia);
}
if (content.hasTypingIndicator()) {
await handleTypingIndicator(
fromUserId,
content.groupId,
content.typingIndicator,
receiptId,
);
}
return const DecryptedMessageResult();
}
String _getEncryptedContentType(EncryptedContent content) {
if (content.hasMessageUpdate()) return 'messageUpdate';
if (content.hasMedia()) return 'media';
if (content.hasMediaUpdate()) return 'mediaUpdate';
if (content.hasContactUpdate()) return 'contactUpdate';
if (content.hasContactRequest()) return 'contactRequest';
if (content.hasFlameSync()) return 'flameSync';
if (content.hasPushKeys()) return 'pushKeys';
if (content.hasReaction()) return 'reaction';
if (content.hasTextMessage()) return 'textMessage';
if (content.hasGroupCreate()) return 'groupCreate';
if (content.hasGroupJoin()) return 'groupJoin';
if (content.hasGroupUpdate()) return 'groupUpdate';
if (content.hasResendGroupPublicKey()) return 'resendGroupPublicKey';
if (content.hasErrorMessages()) return 'errorMessages';
if (content.hasAdditionalDataMessage()) return 'additionalDataMessage';
if (content.hasTypingIndicator()) return 'typingIndicator';
if (content.hasUserDiscoveryRequest()) return 'userDiscoveryRequest';
if (content.hasUserDiscoveryUpdate()) return 'userDiscoveryUpdate';
if (content.hasKeyVerificationProof()) return 'keyVerificationProof';
return 'unknown';
}
class DecryptedMessageResult {
const DecryptedMessageResult({
this.responseCipherText,
this.responsePlaintext,
this.showPushNotification = true,
});
final EncryptedContent? responseCipherText;
final PlaintextContent? responsePlaintext;
final bool showPushNotification;
}

View file

@ -6,30 +6,26 @@ import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'
as client;
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server; as server;
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart'
hide Message; hide Message;
import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/utils/secure_storage.dart';
class Result<T, E> { class Result<T, E> {
Result.error(this.error) : value = null; Result.error(this.error) : value = null, _isSuccess = false;
Result.success(this.value) : error = null; Result.success(this.value) : error = null, _isSuccess = true;
final T? value; final T? value;
final E? error; final E? error;
final bool _isSuccess;
bool get isSuccess => value != null; bool get isSuccess => _isSuccess;
bool get isError => error != null; bool get isError => !_isSuccess;
} }
DateTime fromTimestamp(Int64 timeStamp) { DateTime fromTimestamp(Int64 timeStamp) {
@ -53,22 +49,6 @@ Result asResult(server.ServerToClient? msg) {
} }
} }
ClientToServer createClientToServerFromHandshake(Handshake handshake) {
final v0 = client.V0()
..seq = Int64()
..handshake = handshake;
return ClientToServer()..v0 = v0;
}
ClientToServer createClientToServerFromApplicationData(
ApplicationData applicationData,
) {
final v0 = client.V0()
..seq = Int64()
..applicationdata = applicationData;
return ClientToServer()..v0 = v0;
}
Future<void> handleMediaError(MediaFile media) async { Future<void> handleMediaError(MediaFile media) async {
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId, media.mediaId,
@ -100,11 +80,6 @@ Future<bool> importSignalContactAndCreateRequest(
return false; return false;
} }
// 1. Setup notifications keys with the other user
await setupNotificationWithUsers(
forceContact: userdata.userId.toInt(),
);
// 2. Then send user request // 2. Then send user request
await sendCipherText( await sendCipherText(
userdata.userId.toInt(), userdata.userId.toInt(),

View file

@ -309,8 +309,11 @@ class BackupService {
String username, String username,
String password, String password,
) async { ) async {
final userId = await apiService.getUserIdFromUsername(username); late final int userId;
if (userId == null) { try {
userId = await RustApi.getUserIdFromUsername(username: username);
} catch (error) {
Log.error('Could not resolve backup username', error: error);
return RecoveryError.usernameNotValid; return RecoveryError.usernameNotValid;
} }

View file

@ -171,7 +171,10 @@ class MemoriesCloudService {
return true; return true;
} }
final urls = await apiService.getMemoriesUrl(mediaId, isThumbnail); final urls = await rustApiProtobuf(
RustApi.getMemoriesUrl(mediaId: mediaId, thumbnail: isThumbnail),
decodeMemoriesUrl,
);
if (urls == null || !urls.hasFullDownloadUrl()) return false; if (urls == null || !urls.hasFullDownloadUrl()) return false;
try { try {
@ -205,10 +208,13 @@ class MemoriesCloudService {
} }
final sizeBytes = ms.storedPath.lengthSync(); final sizeBytes = ms.storedPath.lengthSync();
final urls = await apiService.requestMemoriesUpload( final urls = await rustApiProtobuf(
sizeBytes, RustApi.requestMemoriesUpload(
mediaFile.createdAt, size: sizeBytes,
mediaFile.mediaId, originalDate: mediaFile.createdAt.millisecondsSinceEpoch,
mediaId: mediaFile.mediaId,
),
decodeMemoriesUploadUrls,
); );
if (urls == null) { if (urls == null) {
@ -268,8 +274,8 @@ class MemoriesCloudService {
} }
// 3. Confirm upload // 3. Confirm upload
final confirmRes = await apiService.confirmMemoriesUpload( final confirmRes = await rustApiResult(
mediaFile.mediaId, RustApi.confirmMemoriesUpload(mediaId: mediaFile.mediaId),
); );
if (confirmRes.isSuccess) { if (confirmRes.isSuccess) {
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(

View file

@ -98,10 +98,8 @@ Future<void> runMigrations() async {
..canUseLoginTokenForAuth = false ..canUseLoginTokenForAuth = false
// As usernames changes where not considered in the old version force users // As usernames changes where not considered in the old version force users
// to reenter there passwords. // to reenter there passwords.
// ignore: deprecated_member_use_from_same_package ..twonlySafeBackup?.encryptionKey = Uint8List(0)
..twonlySafeBackup?.encryptionKey = [] ..twonlySafeBackup?.backupId = Uint8List(0);
// ignore: deprecated_member_use_from_same_package
..twonlySafeBackup?.backupId = [];
}); });
} }
} }

View file

@ -9,14 +9,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/localization/generated/app_localizations_de.dart'; import 'package:twonly/src/localization/generated/app_localizations_de.dart';
import 'package:twonly/src/localization/generated/app_localizations_en.dart'; import 'package:twonly/src/localization/generated/app_localizations_en.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/providers/routing.provider.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -50,29 +47,6 @@ Future<void> customLocalPushNotification(String title, String msg) async {
); );
} }
Future<void> showPushNotificationFromServerMessages(
int fromUserId,
EncryptedContent encryptedContent,
) async {
final pushData = await getPushNotificationFromEncryptedContent(
null, // this is the toUserID which must be null as this means that the targetMessageId was send from this user.
null,
encryptedContent,
);
if (pushData != null) {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
for (final pushUser in pushUsers) {
if (pushUser.userId.toInt() == fromUserId) {
String? groupId;
if (encryptedContent.hasGroupId()) {
groupId = encryptedContent.groupId;
}
return showLocalPushNotification(pushUser, pushData, groupId: groupId);
}
}
}
}
Future<PushNotification?> tryDecryptMessage( Future<PushNotification?> tryDecryptMessage(
List<int> key, List<int> key,
EncryptedPushNotification push, EncryptedPushNotification push,

View file

@ -36,22 +36,16 @@ class FcmNotificationService {
static Future<void> initFCMAfterAuthenticated({bool force = false}) async { static Future<void> initFCMAfterAuthenticated({bool force = false}) async {
final fcmToken = userService.currentUser.fcmToken; final fcmToken = userService.currentUser.fcmToken;
if (userService.currentUser.updateFCMToken || force) { if (userService.currentUser.updateFcmToken || force) {
if (fcmToken == null) { if (fcmToken == null) {
Log.error('FCM token could not be updated as it is empty'); Log.error('FCM token could not be updated as it is empty');
await _checkForTokenUpdates(); await _checkForTokenUpdates();
return; return;
} }
final res = await apiService.updateFCMToken( if (await _uploadFcmToken(fcmToken)) {
fcmToken,
);
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) { await UserService.update((u) {
u.updateFCMToken = false; u.updateFcmToken = false;
}); });
} else {
Log.error('Could not update FCM token!');
} }
} }
} }
@ -100,18 +94,14 @@ class FcmNotificationService {
Log.info('Got new FCM token.'); Log.info('Got new FCM token.');
await UserService.update((u) { await UserService.update((u) {
u u
..updateFCMToken = true ..updateFcmToken = true
..fcmToken = fcmToken; ..fcmToken = fcmToken;
}); });
if (apiService.isAuthenticated) { if (apiService.isAuthenticated) {
final res = await apiService.updateFCMToken(fcmToken); if (await _uploadFcmToken(fcmToken)) {
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) { await UserService.update((u) {
u.updateFCMToken = false; u.updateFcmToken = false;
}); });
} else {
Log.error('Could not update FCM token!');
} }
} }
} }
@ -121,18 +111,14 @@ class FcmNotificationService {
.listen((String fcmToken) async { .listen((String fcmToken) async {
await UserService.update((u) { await UserService.update((u) {
u u
..updateFCMToken = true ..updateFcmToken = true
..fcmToken = fcmToken; ..fcmToken = fcmToken;
}); });
if (apiService.isAuthenticated) { if (apiService.isAuthenticated) {
final res = await apiService.updateFCMToken(fcmToken); if (await _uploadFcmToken(fcmToken)) {
if (res.isSuccess) {
Log.info('Uploaded new FCM token!');
await UserService.update((u) { await UserService.update((u) {
u.updateFCMToken = false; u.updateFcmToken = false;
}); });
} else {
Log.error('Could not update FCM token!');
} }
} }
}) })
@ -144,6 +130,17 @@ class FcmNotificationService {
} }
} }
static Future<bool> _uploadFcmToken(String token) async {
try {
await RustApi.updateFcmToken(token: token);
Log.info('Uploaded new FCM token!');
return true;
} catch (error) {
Log.error('Could not update FCM token!', error: error);
return false;
}
}
static Future<void> handleRemoteMessage(RemoteMessage message) async { static Future<void> handleRemoteMessage(RemoteMessage message) async {
Log.info('handleRemoteMessage received message: ${message.messageId}'); Log.info('handleRemoteMessage received message: ${message.messageId}');
await _updateLastFcmMessageTimestamp(); await _updateLastFcmMessageTimestamp();

View file

@ -1,446 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hashlib/random.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/secure_storage.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
/// This function must be called after the database is setup
Future<void> setupNotificationWithUsers({
bool force = false,
int? forceContact,
}) async {
var pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
// HotFIX: Search for user with id 0 if not there remove all
// and create new push keys with all users.
final pushUser = pushUsers.firstWhereOrNull((x) => x.userId.toInt() == 0);
if (pushUser == null) {
Log.info('Clearing push keys');
await setPushKeys(SecureStorageKeys.receivingPushKeys, []);
pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys)
..add(
PushUser(
userId: Int64(),
displayName: 'NoUser',
pushKeys: [],
),
);
}
var wasChanged = false;
final random = Random.secure();
final contacts = await twonlyDB.contactsDao.getAllContacts();
for (final contact in contacts) {
final pushUser = pushUsers.firstWhereOrNull(
(x) => x.userId.toInt() == contact.userId,
);
if (pushUser != null && pushUser.pushKeys.isNotEmpty) {
// make it harder to predict the change of the key
final timeBefore = clock.now().subtract(
Duration(days: 10 + random.nextInt(5)),
);
final lastKey = pushUser.pushKeys.last;
final createdAt = DateTime.fromMillisecondsSinceEpoch(
lastKey.createdAtUnixTimestamp.toInt(),
);
if (force ||
(forceContact == contact.userId) ||
createdAt.isBefore(timeBefore)) {
final pushKey = PushKey(
id: lastKey.id + random.nextInt(5),
key: List<int>.generate(32, (index) => random.nextInt(256)),
createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch),
);
await sendNewPushKey(contact.userId, pushKey);
// only store a maximum of two keys
pushUser.pushKeys.clear();
pushUser.pushKeys.add(lastKey);
pushUser.pushKeys.add(pushKey);
wasChanged = true;
Log.info('Creating new pushkey for ${contact.userId}');
}
} else {
Log.info(
'User ${contact.userId} not yet in pushkeys. Creating a new user.',
);
wasChanged = true;
/// Insert a new push user
final pushKey = PushKey(
id: Int64(1),
key: List<int>.generate(32, (index) => random.nextInt(256)),
createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch),
);
await sendNewPushKey(contact.userId, pushKey);
pushUsers.add(
PushUser(
userId: Int64(contact.userId),
displayName: getContactDisplayName(contact),
blocked: contact.blocked,
pushKeys: [pushKey],
),
);
}
}
if (wasChanged) {
await setPushKeys(SecureStorageKeys.receivingPushKeys, pushUsers);
}
}
Future<void> sendNewPushKey(int userId, PushKey pushKey) async {
await sendCipherText(
userId,
EncryptedContent()
..pushKeys = (EncryptedContent_PushKeys()
..type = EncryptedContent_PushKeys_Type.UPDATE
..key = pushKey.key
..keyId = pushKey.id
..createdAt = pushKey.createdAtUnixTimestamp),
);
}
Future<void> updatePushUser(Contact contact) async {
final pushKeys = await getPushKeys(SecureStorageKeys.receivingPushKeys);
final pushUser = pushKeys.firstWhereOrNull(
(x) => x.userId.toInt() == contact.userId,
);
if (pushUser == null) {
pushKeys.add(
PushUser(
userId: Int64(contact.userId),
displayName: getContactDisplayName(contact),
pushKeys: [],
blocked: contact.blocked,
lastMessageId: uuid.v7(),
),
);
} else {
pushUser
..displayName = getContactDisplayName(contact)
..blocked = contact.blocked;
}
await setPushKeys(SecureStorageKeys.receivingPushKeys, pushKeys);
}
Future<void> handleNewPushKey(int fromUserId, int keyId, List<int> key) async {
final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
var pushUser = pushKeys.firstWhereOrNull(
(x) => x.userId.toInt() == fromUserId,
);
if (pushUser == null) {
final contact = await twonlyDB.contactsDao
.getContactByUserId(fromUserId)
.getSingleOrNull();
if (contact == null) return;
pushKeys.add(
PushUser(
userId: Int64(fromUserId),
displayName: getContactDisplayName(contact),
pushKeys: [],
blocked: contact.blocked,
lastMessageId: uuid.v7(),
),
);
pushUser = pushKeys.firstWhereOrNull((x) => x.userId.toInt() == fromUserId);
}
if (pushUser == null) {
Log.error('could not store new push key as no user was found');
}
// only store the newest key...
pushUser!.pushKeys.clear();
pushUser.pushKeys.add(
PushKey(
id: Int64(keyId),
key: key,
createdAtUnixTimestamp: Int64(clock.now().millisecondsSinceEpoch),
),
);
await setPushKeys(SecureStorageKeys.sendingPushKeys, pushKeys);
}
Future<void> updateLastMessageId(int fromUserId, String messageId) async {
final pushUsers = await getPushKeys(SecureStorageKeys.receivingPushKeys);
final pushUser = pushUsers.firstWhereOrNull(
(x) => x.userId.toInt() == fromUserId,
);
if (pushUser == null) {
unawaited(setupNotificationWithUsers());
return;
}
if (isUUIDNewer(messageId, pushUser.lastMessageId)) {
pushUser.lastMessageId = messageId;
await setPushKeys(SecureStorageKeys.receivingPushKeys, pushUsers);
}
}
Future<PushNotification?> getPushNotificationFromEncryptedContent(
int? toUserId,
String? messageId,
EncryptedContent content,
) async {
PushKind? kind;
String? additionalContent;
if (content.hasReaction()) {
if (content.reaction.remove) return null;
final msg = await twonlyDB.messagesDao
.getMessageById(content.reaction.targetMessageId)
.getSingleOrNull();
if (msg == null || msg.senderId != toUserId) {
return null;
}
if (msg.content != null) {
kind = PushKind.REACTION_TO_TEXT;
} else if (msg.mediaId != null) {
final media = await twonlyDB.mediaFilesDao.getMediaFileById(msg.mediaId!);
if (media == null) return null;
switch (media.type) {
case MediaType.image:
kind = PushKind.REACTION_TO_IMAGE;
case MediaType.audio:
kind = PushKind.REACTION_TO_AUDIO;
case MediaType.video:
kind = PushKind.REACTION_TO_VIDEO;
case MediaType.gif:
kind = PushKind.REACTION;
}
}
additionalContent = content.reaction.emoji;
}
if (content.hasTextMessage()) {
kind = PushKind.TEXT;
if (content.textMessage.hasQuoteMessageId()) {
kind = PushKind.RESPONSE;
}
final group = await twonlyDB.groupsDao.getGroup(content.groupId);
if (group != null && !group.isDirectChat) {
additionalContent = group.groupName;
}
}
if (content.hasAdditionalDataMessage()) {
kind = PushKind.TEXT;
}
if (content.hasMedia()) {
switch (content.media.type) {
case EncryptedContent_Media_Type.REUPLOAD:
return null;
case EncryptedContent_Media_Type.IMAGE:
kind = PushKind.IMAGE;
case EncryptedContent_Media_Type.VIDEO:
kind = PushKind.VIDEO;
case EncryptedContent_Media_Type.GIF:
kind = PushKind.IMAGE;
case EncryptedContent_Media_Type.AUDIO:
kind = PushKind.AUDIO;
}
if (content.media.requiresAuthentication) {
kind = PushKind.TWONLY;
}
final group = await twonlyDB.groupsDao.getGroup(content.groupId);
if (group != null && !group.isDirectChat) {
additionalContent = group.groupName;
}
}
if (content.hasContactRequest()) {
switch (content.contactRequest.type) {
case EncryptedContent_ContactRequest_Type.REQUEST:
kind = PushKind.CONTACT_REQUEST;
case EncryptedContent_ContactRequest_Type.ACCEPT:
kind = PushKind.ACCEPT_REQUEST;
case EncryptedContent_ContactRequest_Type.REJECT:
return null;
}
}
if (content.hasMediaUpdate()) {
final msg = await twonlyDB.messagesDao
.getMessageById(content.mediaUpdate.targetMessageId)
.getSingleOrNull();
// These notifications should only be send to the original sender.
if (msg == null || msg.senderId != toUserId) {
return null;
}
switch (content.mediaUpdate.type) {
case EncryptedContent_MediaUpdate_Type.REOPENED:
kind = PushKind.REOPENED_MEDIA;
case EncryptedContent_MediaUpdate_Type.STORED:
kind = PushKind.STORED_MEDIA_FILE;
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
return null;
}
}
if (content.hasGroupCreate()) {
kind = PushKind.ADDED_TO_GROUP;
final group = await twonlyDB.groupsDao.getGroup(content.groupId);
if (group != null) {
additionalContent = group.groupName;
}
}
if (kind == null) return null;
final pushNotification = PushNotification()..kind = kind;
if (additionalContent != null) {
pushNotification.additionalContent = additionalContent;
}
if (messageId != null) {
pushNotification.messageId = messageId;
}
return pushNotification;
}
Future<void> requestNewPushKeysForUser(int toUserId) async {
await sendCipherText(
toUserId,
EncryptedContent()
..pushKeys = (EncryptedContent_PushKeys()
..type = EncryptedContent_PushKeys_Type.REQUEST),
);
}
/// this will trigger a push notification
/// push notification only containing the message kind and username
Future<Uint8List?> encryptPushNotification(
int toUserId,
PushNotification content,
) async {
final pushKeys = await getPushKeys(SecureStorageKeys.sendingPushKeys);
var key = 'InsecureOnlyUsedForAddingContact'.codeUnits;
var keyId = 0;
final pushUser = pushKeys.firstWhereOrNull(
(x) => x.userId.toInt() == toUserId,
);
if (pushUser == null) {
// user does not have send any push keys
// only allow accept request and contact request to be send in an insecure way :/
// In future find a better way, e.g. use the signal protocol in a native way..
if (content.kind != PushKind.ACCEPT_REQUEST &&
content.kind != PushKind.CONTACT_REQUEST &&
content.kind != PushKind.TEST_NOTIFICATION) {
// this will be enforced after every app uses this system... :/
// return null;
Log.warn('Using insecure key as the receiver does not send a push key!');
await requestNewPushKeysForUser(toUserId);
}
} else {
final createdAt = DateTime.fromMillisecondsSinceEpoch(
pushUser.pushKeys.last.createdAtUnixTimestamp.toInt(),
);
final timeBefore = clock.now().subtract(const Duration(days: 8));
if (createdAt.isBefore(timeBefore)) {
await requestNewPushKeysForUser(toUserId);
}
try {
key = pushUser.pushKeys.last.key;
keyId = pushUser.pushKeys.last.id.toInt();
} catch (e) {
Log.error('No push notification key found for user $toUserId');
return null;
}
}
final chacha20 = FlutterChacha20.poly1305Aead();
final nonce = chacha20.newNonce();
final secretBox = await chacha20.encrypt(
content.writeToBuffer(),
secretKey: SecretKeyData(key),
nonce: nonce,
);
final res = EncryptedPushNotification(
keyId: Int64(keyId),
nonce: nonce,
ciphertext: secretBox.cipherText,
mac: secretBox.mac.bytes,
);
return res.writeToBuffer();
}
Future<List<PushUser>> getPushKeys(String storageKey) async {
const storage = FlutterSecureStorage();
try {
final pushKeysProto = await storage.read(
key: storageKey,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
if (pushKeysProto == null) return [];
final pushKeysRaw = base64Decode(pushKeysProto);
return PushUsers.fromBuffer(pushKeysRaw).users;
} catch (e) {
Log.error(e);
}
return [];
}
Future<void> setPushKeys(String storageKey, List<PushUser> pushKeys) async {
const storage = FlutterSecureStorage();
try {
await storage.delete(
key: storageKey,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
} catch (e) {
Log.error(e);
}
final jsonString = base64Encode(PushUsers(users: pushKeys).writeToBuffer());
try {
await storage.write(
key: storageKey,
value: jsonString,
iOptions: const IOSOptions(
groupId: 'CN332ZUGRP.eu.twonly.shared',
accessibility: KeychainAccessibility.first_unlock,
),
);
} catch (e) {
Log.error(e);
}
}

View file

@ -1,23 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert' show base64Url, utf8; import 'dart:convert' show base64Url, utf8;
import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart' hide Hmac;
import 'package:cryptography_plus/cryptography_plus.dart' import 'package:cryptography_plus/cryptography_plus.dart'
show Hkdf, Hmac, Mac, SecretBox, SecretKey, Xchacha20; show Hkdf, Hmac, Mac, SecretBox, SecretKey, Xchacha20;
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'
show Int64List;
import 'package:twonly/core/bridge/wrapper.dart'; import 'package:twonly/core/bridge/wrapper.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/core/user_config.dart' show PasswordlessRecoveryConfig;
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart'; import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart' import 'package:twonly/src/database/daos/contacts.dao.dart'
show getContactDisplayName; show getContactDisplayName;
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/model/json/onboarding_state.model.dart';
import 'package:twonly/src/model/json/userdata.model.dart' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
show PasswordLessRecovery; as server;
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb; as pb;
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
@ -128,11 +128,11 @@ class PasswordlessRecoveryService {
mac: secretBox.mac.bytes, mac: secretBox.mac.bytes,
); );
final res = await apiService.submitRecoveryShare( await RustApi.submitRecoveryShare(
notificationId: notificationId, notificationId: notificationId,
encryptedMessage: envelope.writeToBuffer(), encryptedMessage: envelope.writeToBuffer(),
); );
return res.isSuccess; return true;
} catch (e) { } catch (e) {
Log.error('Failed to submit recovery share', error: e); Log.error('Failed to submit recovery share', error: e);
return false; return false;
@ -172,7 +172,7 @@ class PasswordlessRecoveryService {
await twonlyDB.contactsDao.resetRecoveryDataForAllContacts(); await twonlyDB.contactsDao.resetRecoveryDataForAllContacts();
await UserService.update((u) => u.passwordLessRecovery = null); await UserService.update((u) => u.passwordLessRecovery = null);
final config = PasswordLessRecovery(threshold); final config = PasswordlessRecoveryConfig(threshold: threshold);
final xchacha20 = Xchacha20.poly1305Aead(); final xchacha20 = Xchacha20.poly1305Aead();
// 2. If enabled, handle the second factor and create serverKey // 2. If enabled, handle the second factor and create serverKey
@ -309,7 +309,7 @@ class PasswordlessRecoveryService {
); );
} }
unawaited(performHeartbeat()); unawaited(RustApi.performPasswordlessRecoveryHeartbeat());
// The passwordless is configured successfully. // The passwordless is configured successfully.
return true; return true;
@ -357,193 +357,6 @@ class PasswordlessRecoveryService {
} }
} }
static Future<void> performHeartbeat() async {
final config = userService.currentUser.passwordLessRecovery;
if (config != null) {
final lastHeartbeat = config.lastServerHeartbeat;
final isOlderThanAMonth =
lastHeartbeat != null &&
clock.now().difference(lastHeartbeat).inDays > 20;
if ((lastHeartbeat == null || isOlderThanAMonth) &&
config.encryptedServerKey != null) {
final res = await apiService.registerPasswordLessRecovery(
config.encryptedServerKey!,
config.pinUnlockToken,
);
if (res.isSuccess) {
await UserService.update((u) {
u.passwordLessRecovery?.lastServerHeartbeat = clock.now();
});
}
}
final lastContactHeartbeat = config.lastContactHeartbeat;
final isContactHeartbeatOlderThan24h =
lastContactHeartbeat == null ||
clock.now().difference(lastContactHeartbeat).inHours >= 24;
if (isContactHeartbeatOlderThan24h) {
// Get all contacts where recoveryLastHeartbeat is NULL. Then for each contact send.
// recoveryLastHeartbeat is ONLY updated in case the contact has responded.
final pendingShares =
await (twonlyDB.select(twonlyDB.contacts)..where(
(t) =>
t.recoveryIsTrustedFriend.equals(true) &
t.recoveryLastHeartbeat.isNull() &
t.recoverySecretShare.isNotNull(),
))
.get();
for (final contact in pendingShares) {
try {
await sendCipherText(
contact.userId,
pb.EncryptedContent(
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
recoverySecretShare: contact.recoverySecretShare,
delete: false,
threshold: Int64(config.threshold),
),
),
);
} catch (e) {
Log.error(
'Failed to send PasswordLessRecovery share to contact ${contact.userId}: $e',
);
}
}
await UserService.update((u) {
u.passwordLessRecovery?.lastContactHeartbeat = clock.now();
});
}
}
// Send heartbeat to the friends I am a trusted friend.
final oneWeekAgo = clock.now().subtract(const Duration(days: 7));
final trustedFriendsToNotify =
await (twonlyDB.select(twonlyDB.contacts)..where(
(t) =>
t.recoveryContactsSecretShare.isNotNull() &
(t.recoveryContactsLastHeartbeat.isNull() |
t.recoveryContactsLastHeartbeat.isSmallerThanValue(
oneWeekAgo,
)),
))
.get();
for (final contact in trustedFriendsToNotify) {
try {
final share = contact.recoveryContactsSecretShare!;
final hash = sha256.convert(share).bytes;
await sendCipherText(
contact.userId,
pb.EncryptedContent(
passwordlessRecoveryHeartbeat:
pb.EncryptedContent_PasswordLessRecoveryHeartbeat(
hash: hash,
),
),
);
await twonlyDB.contactsDao.updateContact(
contact.userId,
ContactsCompanion(
recoveryContactsLastHeartbeat: Value(clock.now()),
),
);
} catch (e) {
Log.error(
'Failed to send PasswordLessRecoveryHeartbeat to contact ${contact.userId}: $e',
);
}
}
}
static Future<void> handlePasswordlessRecovery(
int fromUserId,
pb.EncryptedContent_PasswordLessRecovery msg,
String receiptId,
) async {
if (msg.delete) {
Log.info(
'[$receiptId] Received request to delete passwordless recovery share from contact $fromUserId',
);
await twonlyDB.contactsDao.updateContact(
fromUserId,
const ContactsCompanion(
recoveryContactsSecretShare: Value(null),
recoveryContactsLastHeartbeat: Value(null),
),
);
} else if (msg.hasRecoverySecretShare() && msg.hasThreshold()) {
Log.info(
'[$receiptId] Received new passwordless recovery share from contact $fromUserId',
);
await twonlyDB.contactsDao.updateContact(
fromUserId,
ContactsCompanion(
recoveryContactsSecretShare: Value(
Uint8List.fromList(msg.recoverySecretShare),
),
recoveryContactsThreshold: Value(msg.threshold.toInt()),
recoveryContactsLastHeartbeat: const Value(
null, // this will trigger that a heartbeat will be send...
),
),
);
}
unawaited(performHeartbeat());
}
static Future<void> handlePasswordlessRecoveryHeartbeat(
int fromUserId,
pb.EncryptedContent_PasswordLessRecoveryHeartbeat msg,
String receiptId,
) async {
Log.info(
'[$receiptId] Received passwordless recovery heartbeat from contact $fromUserId',
);
final contact = await twonlyDB.contactsDao.getContactById(fromUserId);
final storedShare = contact?.recoverySecretShare;
if (storedShare == null) {
unawaited(
sendCipherText(
fromUserId,
pb.EncryptedContent(
passwordlessRecovery: pb.EncryptedContent_PasswordLessRecovery(
delete: true,
),
),
),
);
Log.warn(
'[$receiptId] Received passwordless recovery heartbeat from $fromUserId but we did not send him a secret share.',
);
return;
}
final computedHash = sha256.convert(storedShare).bytes;
final recoveryLastHeartbeat =
const ListEquality().equals(computedHash, msg.hash)
? clock.now()
: null; // The stored share not valid (maybe an old backup was restored). This will cause the performHeartbeat to resend him his share
Log.info(
'[$receiptId] Got heartbeat: ($recoveryLastHeartbeat)',
);
await twonlyDB.contactsDao.updateContact(
fromUserId,
ContactsCompanion(
recoveryLastHeartbeat: Value(recoveryLastHeartbeat),
),
);
}
static Future<bool> checkAndStorePasswordlessMessages( static Future<bool> checkAndStorePasswordlessMessages(
OnboardingState state, OnboardingState state,
) async { ) async {
@ -554,17 +367,29 @@ class PasswordlessRecoveryService {
return false; return false;
} }
final alreadyReceivedIds = state.receivedShares final alreadyReceivedIds = Int64List.fromList(
.map((s) => Int64(s.messageId)) state.receivedShares.map((share) => share.messageId).toList(),
.toList();
final response = await apiService.checkForPasswordlessNotification(
notificationId: state.notificationId!,
downloadAuthToken: state.downloadAuthToken!,
alreadyReceivedIds: alreadyReceivedIds,
); );
if (response == null || response.messages.isEmpty) { late final server.Response_PasswordlessNotificationMessages response;
try {
final responseBytes = await RustApi.checkForPasswordlessNotification(
notificationId: state.notificationId!,
downloadAuthToken: state.downloadAuthToken!,
alreadyReceivedMessageIds: alreadyReceivedIds,
);
response = server.Response_PasswordlessNotificationMessages.fromBuffer(
responseBytes,
);
} catch (error) {
Log.error(
'Failed to load passwordless recovery messages',
error: error,
);
return false;
}
if (response.messages.isEmpty) {
return false; return false;
} }

View file

@ -1 +0,0 @@
enum SetupProfile { standard, customized }

View file

@ -30,10 +30,12 @@ class SignalIdentityService {
await UserService.update((user) { await UserService.update((user) {
user.signalLastSignedPreKeyUpdated = now; user.signalLastSignedPreKeyUpdated = now;
}); });
final res = await apiService.updateSignedPreKey( final res = await rustApiResult(
signedPreKey.id, RustApi.updateSignedPreKey(
signedPreKey.getKeyPair().publicKey.serialize(), id: signedPreKey.id,
signedPreKey.signature, key: signedPreKey.getKeyPair().publicKey.serialize(),
signature: signedPreKey.signature,
),
); );
if (res.isError) { if (res.isError) {
Log.error('could not update the signed pre key: ${res.error}'); Log.error('could not update the signed pre key: ${res.error}');
@ -52,14 +54,16 @@ class SignalIdentityService {
)) { )) {
final bundle = await RustSignal.generateBundle(); final bundle = await RustSignal.generateBundle();
final pqcRes = await apiService.uploadPqcPreKeys( final pqcRes = await rustApiResult(
bundle.signedPreKeyId, RustApi.uploadPqcPreKeys(
bundle.signedPreKeyPublic, eccSignedPrekeyId: bundle.signedPreKeyId,
bundle.signedPreKeySignature, eccSignedPrekey: bundle.signedPreKeyPublic,
bundle.kyberPreKeyId, eccSignedPrekeySignature: bundle.signedPreKeySignature,
bundle.kyberPreKeyPublic, kyberSignedPrekeyId: bundle.kyberPreKeyId,
bundle.kyberPreKeySignature, kyberSignedPrekey: bundle.kyberPreKeyPublic,
[], kyberSignedPrekeySignature: bundle.kyberPreKeySignature,
prekeys: const [],
),
); );
if (pqcRes.isError) { if (pqcRes.isError) {

View file

@ -206,7 +206,10 @@ Future<Uint8List?> getPublicKeyFromContact(int contactId) async {
} }
Future<bool> handleSessionResync(int fromUserId) async { Future<bool> handleSessionResync(int fromUserId) async {
final userData = await apiService.getUserById(fromUserId); final userData = await rustApiProtobuf(
RustApi.getUserById(userId: fromUserId),
decodeUserData,
);
if (userData != null) { if (userData != null) {
Log.info('Got new session data from the server to re-sync the session'); Log.info('Got new session data from the server to re-sync the session');
return processSignalUserData(userData); return processSignalUserData(userData);

View file

@ -1,16 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/core/bridge/user_config.dart';
import 'package:twonly/core/user_config.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/secure_storage.dart'; import 'package:twonly/src/utils/secure_storage.dart';
class UserService { class UserService {
late UserData currentUser; late UserConfig currentUser;
bool isUserCreated = false; bool isUserCreated = false;
static final Mutex _updateProtection = Mutex(); static final Mutex _updateProtection = Mutex();
@ -18,43 +16,26 @@ class UserService {
Stream<void> get onUserUpdated => _userDataUpdateController.stream; Stream<void> get onUserUpdated => _userDataUpdateController.stream;
Future<bool> tryInit() async { Future<bool> tryInit() async {
final user = await getUser(); final config = await UserConfigApi.load();
if (user == null) return false; if (config == null) return false;
userService.currentUser = user; _applyRustUserConfig(config, notify: false);
userService.isUserCreated = true; return isUserCreated;
return true;
} }
static Future<UserData?> getUser() async { static Future<UserConfig?> getUser() async {
try { try {
// 1. Try to load from KeyValueStore (user.json) final config = await UserConfigApi.load();
final userDataMap = await KeyValueStore.get('user'); if (config != null) return config;
if (userDataMap != null) {
final userData = UserData.fromJson(userDataMap);
await RustKeyManager.setUserId(userId: userData.userId);
try {
// Ensure that the old userData is removed as it breaks the backup mechanism.
// This code can be removed when all users have updated to the latest version...
await SecureStorage.instance.delete(key: 'userData');
} catch (e) {
Log.error('Could not delete user data from SecureStorage: $e');
}
return userData;
}
// 2. If not found, try to load from SecureStorage (Migration path) // One-time migration from the pre-user.json secure-storage format.
final userDataJson = await SecureStorage.instance.read( final userDataJson = await SecureStorage.instance.read(
key: 'userData', key: 'userData',
); );
if (userDataJson != null) { if (userDataJson != null) {
final userData = UserData.fromJson( final migrated = await UserConfigApi.importJson(json: userDataJson);
jsonDecode(userDataJson) as Map<String, dynamic>, await _removeLegacySecureStorageUser();
); return migrated;
// 3. Run migration
await _migrateFromSecureStorage(userData);
return userData;
} }
return null; return null;
@ -64,15 +45,7 @@ class UserService {
} }
} }
static Future<void> _migrateFromSecureStorage(UserData userData) async { static Future<void> _removeLegacySecureStorageUser() async {
await KeyValueStore.put('user', userData.toJson());
try {
await RustKeyManager.setUserId(userId: userData.userId);
} catch (e) {
Log.error('Could not set userId in RustKeyManager during migration: $e');
}
try { try {
await SecureStorage.instance.delete(key: 'userData'); await SecureStorage.instance.delete(key: 'userData');
} catch (e) { } catch (e) {
@ -83,35 +56,51 @@ class UserService {
} }
static Future<void> update( static Future<void> update(
void Function(UserData userData) updateUser, void Function(UserConfig userData) updateUser,
) async { ) async {
await _updateProtection.protect(() async { await _updateProtection.protect(() async {
try { try {
final user = await getUser(); final config = await UserConfigApi.load();
if (user == null) return;
if (config == null) {
throw Exception('User Config is missing');
}
final user = UserConfigApi.clone(config: config);
if (user.defaultShowTime == 999999) { if (user.defaultShowTime == 999999) {
// This was the old version for infinity -> change it to null // This was the old version for infinity -> change it to null
user.defaultShowTime = null; user.defaultShowTime = null;
} }
updateUser(user); updateUser(user);
await KeyValueStore.put('user', user.toJson());
userService.currentUser = user; if (config == user) {
return;
}
final normalized = await UserConfigApi.update(
base: config,
config: user,
);
userService._applyRustUserConfig(normalized);
} catch (e) { } catch (e) {
Log.error('Could not update the user: $e'); Log.error('Could not update the user: $e');
} }
}); });
userService.triggerUserUpdate();
} }
static Future<void> save(UserData user) async { static Future<void> save(UserConfig user) async {
await KeyValueStore.put('user', user.toJson()); final normalized = await UserConfigApi.save(config: user);
try { userService._applyRustUserConfig(normalized);
await RustKeyManager.setUserId(userId: user.userId); }
} catch (e) {
Log.error('Could not set userId in RustKeyManager during save: $e'); static Future<void> handleRustUserConfigChanged(UserConfig config) async {
} userService._applyRustUserConfig(config);
await userService.tryInit(); }
void _applyRustUserConfig(UserConfig config, {bool notify = true}) {
currentUser = config;
isUserCreated = true;
if (notify) triggerUserUpdate();
} }
void triggerUserUpdate() { void triggerUserUpdate() {

View file

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -9,7 +8,6 @@ import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/user_discovery/types.pb.dart';
import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
@ -19,8 +17,9 @@ class UserDiscoveryService {
.getNewAnnouncementsWithoutData(); .getNewAnnouncementsWithoutData();
for (final announcedUser in announcedUsers) { for (final announcedUser in announcedUsers) {
final userdata = await apiService.getUserById( final userdata = await rustApiProtobuf(
announcedUser.announcedUserId, RustApi.getUserById(userId: announcedUser.announcedUserId),
decodeUserData,
); );
if (userdata == null) continue; if (userdata == null) continue;
if (!userdata.publicIdentityKey.equals( if (!userdata.publicIdentityKey.equals(
@ -49,21 +48,6 @@ class UserDiscoveryService {
} }
} }
static bool isContactAllowed(Contact? c) {
if (c == null) return false;
final u = userService.currentUser;
// Only accepted users are allowed.
if (!c.accepted || c.blocked) return false;
if (c.mediaSendCounter < u.requiredSendImages) return false;
if (c.userDiscoveryExcluded) return false;
if (u.userDiscoveryRequiresManualApproval &&
(c.userDiscoveryManualApproved == null ||
!c.userDiscoveryManualApproved!)) {
return false;
}
return true;
}
static bool shouldRequestManualApproval(Contact c) { static bool shouldRequestManualApproval(Contact c) {
final u = userService.currentUser; final u = userService.currentUser;
if (!c.accepted || c.blocked) return false; if (!c.accepted || c.blocked) return false;
@ -79,20 +63,6 @@ class UserDiscoveryService {
required int threshold, required int threshold,
required bool sharePromotion, required bool sharePromotion,
}) async { }) async {
Log.info('UserDiscoveryService: initializeOrUpdate started');
final userId = userService.currentUser.userId;
final publicKey = await getUserPublicKey();
Log.info('UserDiscoveryService: initializing Rust bridge');
await FlutterUserDiscovery.initializeOrUpdate(
callbackId: isolateCallbackId,
threshold: threshold,
userId: userId,
publicKey: publicKey,
sharePromotion: sharePromotion,
).timeout(const Duration(seconds: 8));
Log.info(
'UserDiscoveryService: Rust bridge initialized, updating UserService',
);
await UserService.update( await UserService.update(
(u) => u (u) => u
..isUserDiscoveryEnabled = true ..isUserDiscoveryEnabled = true
@ -120,14 +90,6 @@ class UserDiscoveryService {
return UserDiscoveryVersion.fromBuffer(version); return UserDiscoveryVersion.fromBuffer(version);
} }
static Future<UserDiscoveryVersion?> getContactVersionTyped(
int contactId,
) async {
final contact = await twonlyDB.contactsDao.getContactById(contactId);
if (contact == null || contact.userDiscoveryVersion == null) return null;
return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!);
}
static UserDiscoveryVersion? getContactVersionTypedFromContact( static UserDiscoveryVersion? getContactVersionTypedFromContact(
Contact contact, Contact contact,
) { ) {
@ -135,89 +97,14 @@ class UserDiscoveryService {
return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!); return UserDiscoveryVersion.fromBuffer(contact.userDiscoveryVersion!);
} }
static Future<Uint8List?> shouldRequestNewMessages(
int fromUserId,
List<int> receivedVersion,
) async {
try {
return await FlutterUserDiscovery.shouldRequestNewMessages(
callbackId: isolateCallbackId,
contactId: fromUserId,
version: receivedVersion,
).timeout(const Duration(seconds: 5));
} catch (e) {
Log.error(e);
return null;
}
}
static Future<List<Uint8List>?> getNewMessages(
int fromUserId,
List<int> receivedVersion,
) async {
try {
return await FlutterUserDiscovery.getNewMessages(
callbackId: isolateCallbackId,
contactId: fromUserId,
receivedVersion: receivedVersion,
).timeout(const Duration(seconds: 5));
} catch (e) {
Log.error(e);
return null;
}
}
static Future<void> handleNewMessages(
int fromUserId,
List<Uint8List> messages,
) async {
try {
final verifications = await twonlyDB.keyVerificationDao
.getContactVerification(fromUserId);
return await FlutterUserDiscovery.handleNewMessages(
callbackId: isolateCallbackId,
contactId: fromUserId,
messages: messages,
publicKeyVerifiedTimestamp:
verifications.lastOrNull?.createdAt.millisecondsSinceEpoch,
).timeout(const Duration(seconds: 5));
} catch (e) {
Log.error(e);
}
}
static Future<void> _removeDeletedContacts() async {
final subquery = twonlyDB.selectOnly(twonlyDB.contacts)
..addColumns([twonlyDB.contacts.userId])
..where(twonlyDB.contacts.accountDeleted.equals(true));
await (twonlyDB.update(
twonlyDB.userDiscoveryOwnPromotions,
)..where((t) => t.contactId.isInQuery(subquery))).write(
UserDiscoveryOwnPromotionsCompanion(promotion: Value(Uint8List(0))),
);
}
static Future<void> changeExclusionForContact( static Future<void> changeExclusionForContact(
int contactId, int contactId,
bool exclude, bool exclude,
) async { ) async {
// Remove old versions from the user... await FlutterUserDiscovery.changeExclusionForContact(
await (twonlyDB.update( callbackId: isolateCallbackId,
twonlyDB.userDiscoveryOwnPromotions, contactId: contactId,
)..where((t) => t.contactId.equals(contactId))).write( exclude: exclude,
UserDiscoveryOwnPromotionsCompanion(promotion: Value(Uint8List(0))),
);
await twonlyDB.contactsDao.updateContact(
contactId,
ContactsCompanion(
userDiscoveryExcluded: Value(exclude),
userDiscoveryVersion: const Value(
null, // If the user is included again, this will trigger a new request of his original announcement
),
),
); );
} }
@ -226,35 +113,4 @@ class UserDiscoveryService {
u.isUserDiscoveryEnabled = false; u.isUserDiscoveryEnabled = false;
}); });
} }
static Future<void> verifyInitializationOnStartup() async {
await _removeDeletedContacts();
final configExists = File(
'${AppEnvironment.supportDir}/user_discovery_config.json',
).existsSync();
final hasShares = await (twonlyDB.select(
twonlyDB.userDiscoveryShares,
)..limit(1)).get().then((list) => list.isNotEmpty);
if (userService.currentUser.isUserDiscoveryEnabled &&
(userService.currentUser.userDiscoveryInitializationError ||
!configExists ||
!hasShares)) {
unawaited(() async {
try {
Log.info(
'Retrying UserDiscovery initialization on startup (configExists: $configExists, hasShares: $hasShares)',
);
await initializeOrUpdate(
threshold: userService.currentUser.userDiscoveryThreshold,
sharePromotion: userService.currentUser.userDiscoverySharePromotion,
);
} catch (e) {
Log.error(
'Failed to retry UserDiscovery initialization on startup: $e',
);
}
}());
}
}
} }

View file

@ -88,7 +88,7 @@ class _ChatAskAFriendEntryState extends State<ChatAskAFriendEntry> {
_username = contact.displayName ?? contact.username; _username = contact.displayName ?? contact.username;
} else { } else {
// Fetch from API // Fetch from API
final userdata = await apiService.getUserById(userId); final userdata = await rustApiProtobuf(RustApi.getUserById(userId: userId), decodeUserData);
if (userdata != null) { if (userdata != null) {
_username = utf8.decode(userdata.username); _username = utf8.decode(userdata.username);
} }
@ -122,7 +122,7 @@ class _ChatAskAFriendEntryState extends State<ChatAskAFriendEntry> {
}); });
try { try {
final userId = _data!.askAboutUserId.toInt(); final userId = _data!.askAboutUserId.toInt();
final userdata = await apiService.getUserById(userId); final userdata = await rustApiProtobuf(RustApi.getUserById(userId: userId), decodeUserData);
if (userdata != null) { if (userdata != null) {
await twonlyDB.contactsDao.insertOnConflictUpdate( await twonlyDB.contactsDao.insertOnConflictUpdate(
ContactsCompanion( ContactsCompanion(

View file

@ -117,8 +117,9 @@ class _ContactRowState extends State<_ContactRow> {
}); });
try { try {
final userdata = await apiService.getUserById( final userdata = await rustApiProtobuf(
widget.contact.userId.toInt(), RustApi.getUserById(userId: widget.contact.userId.toInt()),
decodeUserData,
); );
if (userdata == null) return; if (userdata == null) return;

View file

@ -369,7 +369,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}', 'Calling downloadDone for media ID: ${currentMediaLocal.mediaFile.mediaId}',
); );
unawaited( unawaited(
apiService.downloadDone(currentMediaLocal.mediaFile.downloadToken!), RustApi.downloadDone(token: currentMediaLocal.mediaFile.downloadToken!),
); );
if (currentMediaLocal.mediaFile.type == MediaType.video) { if (currentMediaLocal.mediaFile.type == MediaType.video) {

View file

@ -117,7 +117,10 @@ class _SearchUsernameView extends State<AddNewUserView> {
_isLoading = true; _isLoading = true;
}); });
final userdata = await apiService.getUserData(username); final userdata = await rustApiProtobuf(
RustApi.getUserData(username: username),
decodeUserData,
);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {

View file

@ -47,7 +47,7 @@ class FriendSuggestionsComp extends StatelessWidget {
) async { ) async {
Log.info('Requesting user via friend suggestions'); Log.info('Requesting user via friend suggestions');
final userdata = await apiService.getUserById(user.announcedUserId); final userdata = await rustApiProtobuf(RustApi.getUserById(userId: user.announcedUserId), decodeUserData);
if (userdata == null) { if (userdata == null) {
if (context.mounted) { if (context.mounted) {

View file

@ -123,7 +123,7 @@ class _ContactViewState extends State<ContactView> {
Future<void> handleReportUser(Contact contact) async { Future<void> handleReportUser(Contact contact) async {
final reason = await showReportDialog(context, contact); final reason = await showReportDialog(context, contact);
if (reason == null) return; if (reason == null) return;
final res = await apiService.reportUser(contact.userId, reason); final res = await rustApiResult(RustApi.reportUser(userId: contact.userId, reason: reason));
if (!mounted) return; if (!mounted) return;
if (res.isSuccess) { if (res.isSuccess) {
showSnackbar( showSnackbar(
@ -281,7 +281,7 @@ class _ContactViewState extends State<ContactView> {
icon: FontAwesomeIcons.arrowsRotate, icon: FontAwesomeIcons.arrowsRotate,
text: 'Update Connection to V2 (PQXDH)', text: 'Update Connection to V2 (PQXDH)',
onTap: () async { onTap: () async {
final userData = await apiService.getUserById(contact.userId); final userData = await rustApiProtobuf(RustApi.getUserById(userId: contact.userId), decodeUserData);
if (userData != null) { if (userData != null) {
await processSignalUserData(userData); await processSignalUserData(userData);
final updatedContact = await twonlyDB.contactsDao final updatedContact = await twonlyDB.contactsDao

View file

@ -46,7 +46,7 @@ class MemoriesViewState extends State<MemoriesView>
} }
Future<void> _checkUsage() async { Future<void> _checkUsage() async {
final usage = await apiService.getMemoriesUsage(); final usage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage);
if (usage != null && if (usage != null &&
usage.maxBytes > 0 && usage.maxBytes > 0 &&
usage.currentBytes >= usage.maxBytes) { usage.currentBytes >= usage.maxBytes) {
@ -320,7 +320,7 @@ class MemoriesViewState extends State<MemoriesView>
if (item != null) { if (item != null) {
if (isCompletely) { if (isCompletely) {
item.mediaService.fullMediaRemoval(); item.mediaService.fullMediaRemoval();
await apiService.deleteMemory(mediaId); await RustApi.deleteMemory(mediaId: mediaId);
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId); await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
} else { } else {
if (item.mediaService.storedPath.existsSync()) { if (item.mediaService.storedPath.existsSync()) {

View file

@ -177,7 +177,7 @@ class _SynchronizedImageViewerScreenState
if (deleteCompletely) { if (deleteCompletely) {
item.mediaService.fullMediaRemoval(); item.mediaService.fullMediaRemoval();
await apiService.deleteMemory(mediaId); await RustApi.deleteMemory(mediaId: mediaId);
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId); await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
widget.galleryItems.removeAt(_currentIndex); widget.galleryItems.removeAt(_currentIndex);

View file

@ -15,9 +15,6 @@ import 'package:twonly/core/bridge/wrapper.dart' show RustUtils;
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart'; import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/model/json/onboarding_state.model.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart';
import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart'; import 'package:twonly/src/services/passwordless_recovery.service.dart';
@ -262,10 +259,13 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
); );
// Fetch serverKey // Fetch serverKey
final res = await apiService.getServerKeyForPasswordlessRecovery( final res = await rustApiResult(
userId: userId, RustApi.getServerKeyForPasswordlessRecovery(
pinUnlockToken: reconstructed.pinUnlockToken, userId: userId,
pinProtectionKey: await pinKey.extractBytes(), serverKeyProtection: const [],
pinUnlockToken: Uint8List.fromList(reconstructed.pinUnlockToken),
pinProtectionKey: Uint8List.fromList(await pinKey.extractBytes()),
),
); );
if (res.isError) { if (res.isError) {
@ -281,8 +281,7 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
return; return;
} }
final ok = res.value as server.Response_Ok; serverKey = Uint8List.fromList(res.value!);
serverKey = Uint8List.fromList(ok.passwordlessRecoveryServerKey);
} else if (reconstructed.hasEmailHint()) { } else if (reconstructed.hasEmailHint()) {
final state = _onboardingState; final state = _onboardingState;
if (state == null) return; if (state == null) return;
@ -302,10 +301,12 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
} }
// Fetch serverKey (sends recovery email) // Fetch serverKey (sends recovery email)
final res = await apiService.getServerKeyForPasswordlessRecovery( final res = await rustApiResult(
userId: userId, RustApi.getServerKeyForPasswordlessRecovery(
email: email, userId: userId,
serverKeyProtection: reconstructed.serverKeyProtection, email: email,
serverKeyProtection: reconstructed.serverKeyProtection,
),
); );
if (res.isError) { if (res.isError) {
@ -444,11 +445,13 @@ class _RecoverPasswordlessState extends State<RecoverPasswordless> {
Future<void> _registerPasswordlessNotification(OnboardingState state) async { Future<void> _registerPasswordlessNotification(OnboardingState state) async {
final fcmToken = await FirebaseMessaging.instance.getToken(); final fcmToken = await FirebaseMessaging.instance.getToken();
if (!mounted) return; if (!mounted) return;
final res = await apiService.registerPasswordlessNotification( final res = await rustApiResult(
notificationId: state.notificationId!, RustApi.registerPasswordlessNotification(
downloadAuthToken: state.downloadAuthToken!, notificationId: state.notificationId!,
langCode: Localizations.localeOf(context).languageCode, downloadAuthToken: state.downloadAuthToken!,
googleFcm: fcmToken, langCode: Localizations.localeOf(context).languageCode,
googleFcm: fcmToken,
),
); );
if (res.isSuccess) { if (res.isSuccess) {
state.serverRegistered = true; state.serverRegistered = true;

View file

@ -1,15 +1,16 @@
// ignore_for_file: avoid_dynamic_calls // ignore_for_file: avoid_dynamic_calls
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/core/bridge/user_config.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/json/userdata.model.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
@ -77,7 +78,12 @@ class _RegisterViewState extends State<RegisterView> {
if (proofOfWork != null) { if (proofOfWork != null) {
proof = await proofOfWork!; proof = await proofOfWork!;
} else { } else {
final (pow, registrationDisabled) = await apiService.getProofOfWork(); final proofResult = await rustApiResult(RustApi.getProofOfWork());
final pow = proofResult.value == null
? null
: decodeProofOfWork(proofResult.value!);
final registrationDisabled =
proofResult.error == ErrorCode.RegistrationDisabled;
if (pow == null) { if (pow == null) {
setState(() { setState(() {
_registrationDisabled = registrationDisabled; _registrationDisabled = registrationDisabled;
@ -97,10 +103,17 @@ class _RegisterViewState extends State<RegisterView> {
var userId = 0; var userId = 0;
final res = await apiService.register(username, null, proof); final res = await rustApiResult(
RustApi.register(
username: username,
proofOfWork: proof,
langCode: ui.PlatformDispatcher.instance.locale.languageCode,
isIos: Platform.isIOS,
),
);
if (res.isSuccess) { if (res.isSuccess) {
Log.info('Got user_id ${res.value} from server'); Log.info('Got user_id ${res.value} from server');
userId = res.value.userid.toInt() as int; userId = res.value!;
} else { } else {
proofOfWork = null; proofOfWork = null;
if (res.error == ErrorCode.RegistrationDisabled) { if (res.error == ErrorCode.RegistrationDisabled) {
@ -120,7 +133,7 @@ class _RegisterViewState extends State<RegisterView> {
setState(() { setState(() {
_usernameErrorText = errorCodeToText( _usernameErrorText = errorCodeToText(
context, context,
res.error as ErrorCode, res.error!,
); );
_isTryingToRegister = false; _isTryingToRegister = false;
}); });
@ -141,7 +154,7 @@ class _RegisterViewState extends State<RegisterView> {
await showAlertDialog( await showAlertDialog(
context, context,
'Oh no!', 'Oh no!',
errorCodeToText(context, res.error as ErrorCode), errorCodeToText(context, res.error!),
); );
} }
return; return;
@ -151,11 +164,10 @@ class _RegisterViewState extends State<RegisterView> {
_isTryingToRegister = false; _isTryingToRegister = false;
}); });
final userData = UserData( final userData = await UserConfigApi.create(
userId: userId, userId: userId,
username: username, username: username,
displayName: username, displayName: username,
subscriptionPlan: 'Free',
currentSetupPage: SetupPages.profile.name, currentSetupPage: SetupPages.profile.name,
appVersion: AppState.latestAppVersionId, appVersion: AppState.latestAppVersionId,
); );

View file

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/core/user_config.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/services/profile.service.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart';

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/services/profile.service.dart'; import 'package:twonly/core/user_config.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class SafetyProfileCard extends StatelessWidget { class SafetyProfileCard extends StatelessWidget {

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/core/user_config.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/services/profile.service.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';

View file

@ -56,7 +56,7 @@ class AccountView extends StatelessWidget {
context.lang.settingsAccountDeleteModalBody, context.lang.settingsAccountDeleteModalBody,
); );
if (ok) { if (ok) {
final res = await apiService.deleteAccount(); final res = await rustApiResult(RustApi.deleteAccount());
if (res.isError) { if (res.isError) {
if (!context.mounted) return; if (!context.mounted) return;
showSnackbar( showSnackbar(

View file

@ -54,7 +54,7 @@ class _BackupViewState extends State<BackupView> {
Future<void> _loadBackupStatus() async { Future<void> _loadBackupStatus() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
final status = await BackupService.getData(); final status = await BackupService.getData();
final memoriesUsage = await apiService.getMemoriesUsage(); final memoriesUsage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_backupStatus = status; _backupStatus = status;

View file

@ -82,7 +82,7 @@ Future<bool> promptAndDisableMemoriesBackup(BuildContext context) async {
if (confirmed == true) { if (confirmed == true) {
try { try {
await apiService.disableMemoriesBackup(); await RustApi.disableMemoriesBackup();
final allMedias = await (twonlyDB.select( final allMedias = await (twonlyDB.select(
twonlyDB.mediaFiles, twonlyDB.mediaFiles,
)..where((t) => t.stored.equals(true))).get(); )..where((t) => t.stored.equals(true))).get();

View file

@ -29,7 +29,7 @@ class _MemoriesBackupDetailViewState extends State<MemoriesBackupDetailView> {
} }
Future<void> _loadStats() async { Future<void> _loadStats() async {
final memoriesUsage = await apiService.getMemoriesUsage(); final memoriesUsage = await rustApiProtobuf(RustApi.getMemoriesUsage(), decodeMemoriesUsage);
if (mounted) { if (mounted) {
setState(() { setState(() {
_memoriesUsage = memoriesUsage; _memoriesUsage = memoriesUsage;

View file

@ -335,7 +335,7 @@ class _StorageContentsViewState extends State<StorageContentsView> {
try { try {
if (deleteCompletely) { if (deleteCompletely) {
await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId);
unawaited(apiService.deleteMemory(file.mediaId)); unawaited(RustApi.deleteMemory(mediaId: file.mediaId));
MediaFileService(file).fullMediaRemoval(); MediaFileService(file).fullMediaRemoval();
} else { } else {
MediaFileService(file).storedPath.deleteSync(); MediaFileService(file).storedPath.deleteSync();
@ -384,7 +384,7 @@ class _StorageContentsViewState extends State<StorageContentsView> {
for (final file in selectedFiles) { for (final file in selectedFiles) {
if (deleteCompletely) { if (deleteCompletely) {
await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId); await twonlyDB.mediaFilesDao.deleteMediaFile(file.mediaId);
unawaited(apiService.deleteMemory(file.mediaId)); unawaited(RustApi.deleteMemory(mediaId: file.mediaId));
MediaFileService(file).fullMediaRemoval(); MediaFileService(file).fullMediaRemoval();
} else { } else {
MediaFileService(file).storedPath.deleteSync(); MediaFileService(file).storedPath.deleteSync();

View file

@ -7,10 +7,7 @@ import 'package:hashlib/random.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
as pb;
import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
@ -186,24 +183,7 @@ class _RetransmissionDataViewState extends State<RetransmissionDataView> {
Text( Text(
'MessageId: ${retrans.receipt.messageId}', 'MessageId: ${retrans.receipt.messageId}',
), ),
if (retrans.receipt.messageId != null)
FutureBuilder(
future: getPushNotificationFromEncryptedContent(
retrans.receipt.contactId,
retrans.receipt.messageId,
pb.EncryptedContent.fromBuffer(
pb.Message.fromBuffer(
retrans.receipt.message,
).encryptedContent,
),
),
builder: (d, a) {
if (!a.hasData) return Container();
return Text(
'PushKind: ${a.data?.kind}',
);
},
),
Text( Text(
'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}', 'Retry: ${retrans.receipt.retryCount} : ${retrans.receipt.lastRetry}',
), ),

View file

@ -1,14 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hashlib/random.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
@ -47,8 +42,6 @@ class _NotificationViewState extends State<NotificationView> {
await FcmNotificationService.initFCMAfterAuthenticated(force: true); await FcmNotificationService.initFCMAfterAuthenticated(force: true);
await setupNotificationWithUsers(force: true);
if (!mounted) return; if (!mounted) return;
if (userService.currentUser.fcmToken == null) { if (userService.currentUser.fcmToken == null) {
@ -66,18 +59,6 @@ class _NotificationViewState extends State<NotificationView> {
); );
if (run) { if (run) {
final pushData = await encryptPushNotification(
userService.currentUser.userId,
PushNotification(
messageId: uuid.v4(),
kind: PushKind.TEST_NOTIFICATION,
),
);
await apiService.sendTextMessage(
userService.currentUser.userId,
Uint8List(0),
pushData,
);
_troubleshootingDidRun = true; _troubleshootingDidRun = true;
} }
} }

View file

@ -6,7 +6,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
@ -62,7 +61,9 @@ class _ProfileViewState extends State<ProfileView> {
filteredUsername = filteredUsername.substring(0, 12); filteredUsername = filteredUsername.substring(0, 12);
} }
final result = await apiService.changeUsername(filteredUsername); final result = await rustApiResult(
RustApi.changeUsername(username: filteredUsername),
);
if (result.isError) { if (result.isError) {
if (!mounted) return; if (!mounted) return;

View file

@ -1,12 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart';
import 'package:twonly/src/providers/purchases.provider.dart'; import 'package:twonly/src/providers/purchases.provider.dart';
import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/services/subscription.service.dart';
@ -50,7 +48,10 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
Future<void> initAsync({required bool force}) async { Future<void> initAsync({required bool force}) async {
if (force) { if (force) {
ballance = await apiService.loadPlanBalance(); ballance = await rustApiProtobuf(
RustApi.loadPlanBalance(),
decodePlanBalance,
);
_unusedAdditionalAccounts = _unusedAdditionalAccounts =
_planLimit - (ballance?.additionalAccounts.length ?? _planLimit); _planLimit - (ballance?.additionalAccounts.length ?? _planLimit);
} }
@ -72,7 +73,9 @@ class _AdditionalUsersViewState extends State<AdditionalUsersView> {
as List<int>?; as List<int>?;
if (selectedUserIds == null) return; if (selectedUserIds == null) return;
for (final selectedUserId in selectedUserIds) { for (final selectedUserId in selectedUserIds) {
final res = await apiService.addAdditionalUser(Int64(selectedUserId)); final res = await rustApiResult(
RustApi.addAdditionalUser(userId: selectedUserId),
);
if (res.isError && mounted) { if (res.isError && mounted) {
final contact = await twonlyDB.contactsDao.getContactById( final contact = await twonlyDB.contactsDao.getContactById(
selectedUserId, selectedUserId,
@ -220,8 +223,10 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
context.lang.additionalUserRemoveDesc, context.lang.additionalUserRemoveDesc,
); );
if (remove) { if (remove) {
final res = await apiService.removeAdditionalUser( final res = await rustApiResult(
widget.account.userId, RustApi.removeAdditionalUser(
userId: widget.account.userId.toInt(),
),
); );
if (!context.mounted) return; if (!context.mounted) return;
if (res.isSuccess) { if (res.isSuccess) {
@ -231,7 +236,7 @@ class _AdditionalAccountState extends State<AdditionalAccount> {
context, context,
errorCodeToText( errorCodeToText(
context, context,
res.error as ErrorCode, res.error!,
), ),
); );
} }

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -35,7 +36,10 @@ class _SubscriptionViewState extends State<SubscriptionView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
ballance = await apiService.loadPlanBalance(); ballance = await rustApiProtobuf(
RustApi.loadPlanBalance(),
decodePlanBalance,
);
if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) { if (ballance != null && ballance!.hasAdditionalAccountOwnerId()) {
final ownerId = ballance!.additionalAccountOwnerId.toInt(); final ownerId = ballance!.additionalAccountOwnerId.toInt();
final contact = await twonlyDB.contactsDao final contact = await twonlyDB.contactsDao
@ -49,7 +53,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
} }
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
await apiService.forceIpaCheck(); await RustApi.forceIpaCheck();
} }
@override @override

View file

@ -866,7 +866,7 @@ packages:
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
freezed_annotation: freezed_annotation:
dependency: "direct main" dependency: transitive
description: description:
name: freezed_annotation name: freezed_annotation
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"

View file

@ -258,4 +258,3 @@ flutter:
fonts: fonts:
- asset: assets/fonts/NotoColorEmoji.ttf - asset: assets/fonts/NotoColorEmoji.ttf

2
rust/Cargo.lock generated
View file

@ -97,6 +97,7 @@ dependencies = [
"anyhow", "anyhow",
"atomic", "atomic",
"backtrace", "backtrace",
"chrono",
] ]
[[package]] [[package]]
@ -1093,6 +1094,7 @@ dependencies = [
"build-target", "build-target",
"bytemuck", "bytemuck",
"byteorder", "byteorder",
"chrono",
"console_error_panic_hook", "console_error_panic_hook",
"dart-sys", "dart-sys",
"delegate-attr", "delegate-attr",

View file

@ -7,7 +7,7 @@ edition = "2021"
crate-type = ["cdylib", "staticlib", "rlib"] crate-type = ["cdylib", "staticlib", "rlib"]
[dependencies] [dependencies]
flutter_rust_bridge = "=2.12.0" flutter_rust_bridge = { version = "=2.12.0", features = ["chrono"] }
thiserror = "2.0.18" thiserror = "2.0.18"
sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [ sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [
"runtime-tokio", "runtime-tokio",

View file

@ -25,7 +25,7 @@ fn main() -> Result<()> {
.include_file("websocket_protocol.rs") .include_file("websocket_protocol.rs")
.compile_protos(&websocket_protos, &[websocket_proto_root])?; .compile_protos(&websocket_protos, &[websocket_proto_root])?;
prost_build::compile_protos(&["src/user_discovery/types.proto"], &["src/"])?; prost_build::compile_protos(&["models/user_discovery.proto"], &["src/"])?;
prost_build::Config::new() prost_build::Config::new()
.include_file("client_messages.rs") .include_file("client_messages.rs")
.compile_protos( .compile_protos(

View file

@ -91,11 +91,10 @@ async fn verify_shared_contacts(
.await?; .await?;
let verified_at = chrono::Utc::now().timestamp_millis(); let verified_at = chrono::Utc::now().timestamp_millis();
ctx.get_user_discovery() ctx.get_user_discovery()
.get() .get()
.await .await
.update_verification_state_for_user(contact.user_id, Some(verified_at)) .update_verification_state_for_user(contact.user_id, Some(verified_at), tr)
.await?; .await?;
tracing::info!("verified a contact from shared additional data"); tracing::info!("verified a contact from shared additional data");

View file

@ -17,8 +17,68 @@ use crate::error::{Result, TwonlyError};
use crate::user_config::UserConfig; use crate::user_config::UserConfig;
use encrypted_content::contact_request::Type; use encrypted_content::contact_request::Type;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
use std::collections::HashMap;
use std::io::Write as _; use std::io::Write as _;
use std::sync::Arc; use std::sync::{Arc, Mutex, OnceLock};
static REQUESTED_PROFILES: OnceLock<Mutex<HashMap<i64, i64>>> = OnceLock::new();
pub(crate) async fn check_for_profile_update(
t: &mut Transaction<'_, Sqlite>,
from_user_id: i64,
content: &EncryptedContent,
) -> Result<()> {
let Some(sender_profile_counter) = content.sender_profile_counter else {
return Ok(());
};
let current_counter = sqlx::query_scalar!(
"SELECT sender_profile_counter FROM contacts WHERE user_id = ?",
from_user_id,
)
.fetch_optional(&mut **t)
.await?
.unwrap_or(0);
if content.contact_update.is_some() || sender_profile_counter <= current_counter {
return Ok(());
}
let should_request = {
let mut requested = REQUESTED_PROFILES
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap();
let last_requested = requested.get(&from_user_id).copied().unwrap_or(0);
if sender_profile_counter > last_requested {
requested.insert(from_user_id, sender_profile_counter);
true
} else {
false
}
};
if should_request {
queue_encrypted_content(
t,
from_user_id,
EncryptedContent {
contact_update: Some(encrypted_content::ContactUpdate {
r#type: encrypted_content::contact_update::Type::Request as i32,
username: None,
display_name: None,
avatar_svg_compressed: None,
}),
..Default::default()
},
true,
)
.await?;
}
Ok(())
}
pub(crate) async fn handle_contact_request( pub(crate) async fn handle_contact_request(
ctx: &Arc<Context>, ctx: &Arc<Context>,
@ -73,6 +133,7 @@ pub(crate) async fn handle_contact_request(
))); )));
} }
}; };
let username = user let username = user
.username .username
.ok_or_else(|| TwonlyError::Generic("user response has no username".into()))?; .ok_or_else(|| TwonlyError::Generic("user response has no username".into()))?;
@ -149,8 +210,8 @@ pub(crate) async fn handle_contact_update(
EncryptedContent { EncryptedContent {
contact_update: Some(encrypted_content::ContactUpdate { contact_update: Some(encrypted_content::ContactUpdate {
r#type: encrypted_content::contact_update::Type::Update as i32, r#type: encrypted_content::contact_update::Type::Update as i32,
username: user.username, username: Some(user.username),
display_name: user.display_name, display_name: Some(user.display_name),
avatar_svg_compressed, avatar_svg_compressed,
}), }),
sender_profile_counter: Some(user.avatar_counter), sender_profile_counter: Some(user.avatar_counter),

View file

@ -4,19 +4,25 @@
*/ */
use crate::api::proto::client::encrypted_content; use crate::api::proto::client::encrypted_content;
use crate::context::Context;
use crate::database::app::tables::UpdateContact; use crate::database::app::tables::UpdateContact;
use crate::error::Result; use crate::error::Result;
use crate::services::groups::GroupService;
use encrypted_content::error_messages::Type; use encrypted_content::error_messages::Type;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
use std::sync::Arc;
pub(crate) async fn handle_error_message( pub(crate) async fn handle_error_message(
transaction: &mut Transaction<'_, Sqlite>, ctx: &Arc<Context>,
t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
_receipt_id: &str,
group_id: Option<&str>, group_id: Option<&str>,
error: encrypted_content::ErrorMessages, error: encrypted_content::ErrorMessages,
) -> Result<()> { ) -> Result<()> {
match Type::try_from(error.r#type)? { let error_type = Type::try_from(error.r#type)?;
tracing::warn!(?error_type, from_user_id, "received client error message");
match error_type {
Type::ErrorProcessingMessageCreatedAccountRequestInstead => { Type::ErrorProcessingMessageCreatedAccountRequestInstead => {
sqlx::query!( sqlx::query!(
r#" r#"
@ -27,30 +33,27 @@ pub(crate) async fn handle_error_message(
error.related_receipt_id, error.related_receipt_id,
from_user_id, from_user_id,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
UpdateContact::builder() UpdateContact::builder()
.user_id(from_user_id) .user_id(from_user_id)
.accepted(false) .accepted(false)
.requested(true) .requested(true)
.build() .build()
.update(transaction) .update(t)
.await?; .await?;
} }
Type::GroupNotFoundOrNotAMember => { Type::GroupNotFoundOrNotAMember => {
if let Some(group_id) = group_id { if let Some(group_id) = group_id {
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { GroupService::new(ctx)
let group_id = group_id.to_owned(); .handle_membership_error(
let related_receipt_id = error.related_receipt_id.clone(); t,
tokio::spawn(async move { from_user_id,
(callbacks.api.group_membership_error)( group_id.to_owned(),
from_user_id, error.related_receipt_id,
group_id, )
related_receipt_id, .await?;
)
.await;
});
}
} }
} }
Type::SessionOutOfSync | Type::UnknownMessageType => {} Type::SessionOutOfSync | Type::UnknownMessageType => {}

View file

@ -3,31 +3,25 @@
* *
*/ */
use std::sync::Arc;
use crate::api::messages::incoming::client2client::messages::queue_encrypted_content;
use crate::api::proto::client::encrypted_content; use crate::api::proto::client::encrypted_content;
use crate::context::Context;
use crate::database::app::tables::{Contact, Group};
use crate::error::{Result, TwonlyError}; use crate::error::{Result, TwonlyError};
use crate::utils::{milliseconds_to_seconds, new_uuid_v4}; use crate::services::groups::GroupService;
use crate::utils::{is_today, milliseconds_to_seconds, new_uuid_v4};
use rand::SeedableRng; use rand::SeedableRng;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
pub(crate) async fn ensure_group_member( pub(crate) async fn ensure_group_member(
transaction: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
group_id: &str, group_id: &str,
) -> Result<()> { ) -> Result<()> {
let allowed = sqlx::query_scalar!( let allowed = Group::is_member(t, group_id, from_user_id).await?;
r#"
SELECT EXISTS(
SELECT 1
FROM group_members
WHERE group_id = ? AND contact_id = ?
)
"#,
group_id,
from_user_id,
)
.fetch_one(&mut **transaction)
.await?
!= 0;
if !allowed { if !allowed {
return Err(TwonlyError::Generic(format!( return Err(TwonlyError::Generic(format!(
"user {from_user_id} is not a member of group {group_id}" "user {from_user_id} is not a member of group {group_id}"
@ -37,32 +31,19 @@ pub(crate) async fn ensure_group_member(
} }
pub(crate) async fn handle_group_create( pub(crate) async fn handle_group_create(
transaction: &mut Transaction<'_, Sqlite>, ctx: &std::sync::Arc<crate::context::Context>,
t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
group_id: &str, group_id: &str,
create: encrypted_content::GroupCreate, create: encrypted_content::GroupCreate,
) -> Result<()> { ) -> Result<()> {
let contact_exists = sqlx::query_scalar!( Contact::ensure_exists(t, from_user_id).await?;
r#"
SELECT EXISTS(
SELECT 1 FROM contacts WHERE user_id = ?
)
"#,
from_user_id,
)
.fetch_one(&mut **transaction)
.await?
!= 0;
if !contact_exists {
return Err(TwonlyError::Generic(
"only known contacts may create a group".into(),
));
}
let mut rng = rand::rngs::StdRng::from_os_rng(); let mut rng = rand::rngs::StdRng::from_os_rng();
let identity = libsignal_protocol::IdentityKeyPair::generate(&mut rng); let identity = libsignal_protocol::IdentityKeyPair::generate(&mut rng);
let private_key = identity.serialize().to_vec(); let private_key = identity.serialize().to_vec();
let group_name = create.group_name.unwrap_or_default(); let group_name = create.group_name.unwrap_or_default();
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO groups( INSERT INTO groups(
@ -83,8 +64,9 @@ pub(crate) async fn handle_group_create(
private_key, private_key,
group_name, group_name,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO group_members(group_id, contact_id, group_public_key) INSERT INTO group_members(group_id, contact_id, group_public_key)
@ -96,8 +78,9 @@ pub(crate) async fn handle_group_create(
from_user_id, from_user_id,
create.group_public_key, create.group_public_key,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
sqlx::query!( sqlx::query!(
r#"INSERT INTO group_histories(group_history_id, group_id, contact_id, type) r#"INSERT INTO group_histories(group_history_id, group_id, contact_id, type)
VALUES (?, ?, ?, 'addMember')"#, VALUES (?, ?, ?, 'addMember')"#,
@ -105,42 +88,24 @@ pub(crate) async fn handle_group_create(
group_id, group_id,
from_user_id, from_user_id,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
let group_id = group_id.to_owned(); GroupService::new(ctx)
tokio::spawn(async move { .refresh_group_state(t, group_id.to_owned(), true)
(callbacks.api.group_state_refresh)(group_id, true).await; .await;
});
}
Ok(()) Ok(())
} }
pub(crate) async fn handle_group_join( pub(crate) async fn handle_group_join(
transaction: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
group_id: &str, group_id: &str,
join: encrypted_content::GroupJoin, join: encrypted_content::GroupJoin,
) -> Result<()> { ) -> Result<()> {
let group_exists = sqlx::query_scalar!( Contact::ensure_exists(t, from_user_id).await?;
r#"SELECT EXISTS(SELECT 1 FROM groups WHERE group_id = ?)"#, Group::ensure_exists(t, group_id).await?;
group_id,
)
.fetch_one(&mut **transaction)
.await?
!= 0;
let contact_exists = sqlx::query_scalar!(
r#"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ?)"#,
from_user_id,
)
.fetch_one(&mut **transaction)
.await?
!= 0;
if !group_exists || !contact_exists {
return Err(TwonlyError::Generic(
"group join arrived before group/contact state".into(),
));
}
sqlx::query!( sqlx::query!(
r#" r#"
@ -153,13 +118,14 @@ pub(crate) async fn handle_group_join(
from_user_id, from_user_id,
join.group_public_key, join.group_public_key,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
Ok(()) Ok(())
} }
pub(crate) async fn handle_resend_group_public_key( pub(crate) async fn handle_resend_group_public_key(
transaction: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
group_id: &str, group_id: &str,
) -> Result<()> { ) -> Result<()> {
@ -171,18 +137,19 @@ pub(crate) async fn handle_resend_group_public_key(
"#, "#,
group_id, group_id,
) )
.fetch_optional(&mut **transaction) .fetch_optional(&mut **t)
.await? .await?
.flatten(); .flatten();
let Some(private_key) = private_key else { let Some(private_key) = private_key else {
return Err(TwonlyError::Generic(format!( return Err(TwonlyError::Generic(format!(
"cannot resend the group public key for {group_id} to {from_user_id}" "cannot resend the group public key for {group_id} to {from_user_id}"
))); )));
}; };
let identity = libsignal_protocol::IdentityKeyPair::try_from(private_key.as_slice())
.map_err(|error| TwonlyError::Signal(error.to_string()))?; let identity = libsignal_protocol::IdentityKeyPair::try_from(private_key.as_slice())?;
super::messages::queue_encrypted_content( queue_encrypted_content(
transaction, t,
from_user_id, from_user_id,
crate::api::proto::client::EncryptedContent { crate::api::proto::client::EncryptedContent {
group_id: Some(group_id.to_owned()), group_id: Some(group_id.to_owned()),
@ -194,31 +161,25 @@ pub(crate) async fn handle_resend_group_public_key(
true, true,
) )
.await?; .await?;
Ok(()) Ok(())
} }
pub(crate) async fn handle_group_update( pub(crate) async fn handle_group_update(
transaction: &mut Transaction<'_, Sqlite>, ctx: &Arc<Context>,
t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
group_id: &str, group_id: &str,
update: encrypted_content::GroupUpdate, update: encrypted_content::GroupUpdate,
) -> Result<()> { ) -> Result<()> {
let is_direct = sqlx::query_scalar!( let is_direct = Group::is_direct_chat(t, group_id).await?;
"SELECT is_direct_chat FROM groups WHERE group_id = ?",
group_id
)
.fetch_optional(&mut **transaction)
.await?
.unwrap_or(0)
!= 0;
if !is_direct { if !is_direct {
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { GroupService::new(ctx)
let group_id = group_id.to_owned(); .refresh_group_state(t, group_id.to_owned(), false)
tokio::spawn(async move { .await;
(callbacks.api.group_state_refresh)(group_id, false).await;
});
}
} }
if update.group_action_type == "updatedGroupName" { if update.group_action_type == "updatedGroupName" {
sqlx::query!( sqlx::query!(
r#" r#"
@ -229,7 +190,7 @@ pub(crate) async fn handle_group_update(
update.new_group_name, update.new_group_name,
group_id, group_id,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
} else if update.group_action_type == "changeDisplayMaxTime" && is_direct { } else if update.group_action_type == "changeDisplayMaxTime" && is_direct {
sqlx::query!( sqlx::query!(
@ -243,7 +204,7 @@ pub(crate) async fn handle_group_update(
update.new_delete_messages_after_milliseconds, update.new_delete_messages_after_milliseconds,
group_id, group_id,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
} }
@ -267,43 +228,66 @@ pub(crate) async fn handle_group_update(
update.new_delete_messages_after_milliseconds, update.new_delete_messages_after_milliseconds,
update.group_action_type, update.group_action_type,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
Ok(()) Ok(())
} }
pub(crate) async fn handle_flame_sync( pub(crate) async fn handle_flame_sync(
transaction: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
group_id: &str, group_id: &str,
flame: encrypted_content::FlameSync, flame: encrypted_content::FlameSync,
) -> Result<()> { ) -> Result<()> {
let last_flame_counter_change = milliseconds_to_seconds(flame.last_flame_counter_change); let last_flame_counter_change = milliseconds_to_seconds(flame.last_flame_counter_change);
let Some(group) = sqlx::query!(
r#"
SELECT last_flame_counter_change, flame_counter, max_flame_counter
FROM groups
WHERE group_id = ?
"#,
group_id,
)
.fetch_optional(&mut **t)
.await?
else {
return Ok(());
};
let Some(group_last_flame_counter_change) = group.last_flame_counter_change else {
return Ok(());
};
let update_counters = flame.force_update
|| (is_today(group_last_flame_counter_change) && is_today(last_flame_counter_change));
let flame_counter = if update_counters {
group.flame_counter.max(flame.flame_counter)
} else {
group.flame_counter
};
let max_flame_counter = if update_counters {
group.max_flame_counter.max(flame.flame_counter)
} else {
group.max_flame_counter
};
sqlx::query!( sqlx::query!(
r#" r#"
UPDATE groups UPDATE groups
SET also_best_friend = ?, SET also_best_friend = ?,
flame_counter = CASE WHEN ( flame_counter = ?,
(date(last_flame_counter_change, 'unixepoch', 'localtime') = date('now', 'localtime') max_flame_counter = ?
AND date(?, 'unixepoch', 'localtime') = date('now', 'localtime')) WHERE group_id = ?
OR ?
) THEN MAX(flame_counter, ?) ELSE flame_counter END,
max_flame_counter = CASE WHEN (
(date(last_flame_counter_change, 'unixepoch', 'localtime') = date('now', 'localtime')
AND date(?, 'unixepoch', 'localtime') = date('now', 'localtime'))
OR ?
) THEN MAX(max_flame_counter, ?) ELSE max_flame_counter END
WHERE group_id = ? AND last_flame_counter_change IS NOT NULL
"#, "#,
flame.best_friend, flame.best_friend,
last_flame_counter_change, flame_counter,
flame.force_update, max_flame_counter,
flame.flame_counter,
last_flame_counter_change,
flame.force_update,
flame.flame_counter,
group_id, group_id,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
Ok(()) Ok(())
} }

View file

@ -5,13 +5,31 @@
use crate::api::proto::client::encrypted_content; use crate::api::proto::client::encrypted_content;
use crate::bridge::callbacks::get_callbacks; use crate::bridge::callbacks::get_callbacks;
use crate::context::Context;
use crate::database::app::tables::Group; use crate::database::app::tables::Group;
use crate::error::{Result, TwonlyError}; use crate::error::{Result, TwonlyError};
use crate::services::mediafiles::MediaFileService;
use crate::utils::{milliseconds_to_seconds, new_uuid_v4}; use crate::utils::{milliseconds_to_seconds, new_uuid_v4};
use encrypted_content::media::Type as MediaType; use encrypted_content::media::Type as MediaType;
use encrypted_content::media_update::Type as MediaUpdateType; use encrypted_content::media_update::Type as MediaUpdateType;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
fn spawn_media_download(media_id: String) {
let Ok(ctx) = Context::get_static() else {
tracing::warn!(media_id, "could not start media download without context");
return;
};
let ctx = ctx.clone();
tokio::spawn(async move {
if let Err(error) = MediaFileService::new(&ctx)
.download_when_available(&media_id)
.await
{
tracing::warn!(media_id, %error, "media download failed");
}
});
}
fn spawn_media_action(kind: &'static str, media_id: String, contact_id: i64, message_id: String) { fn spawn_media_action(kind: &'static str, media_id: String, contact_id: i64, message_id: String) {
if let Ok(callbacks) = get_callbacks() { if let Ok(callbacks) = get_callbacks() {
tokio::spawn(async move { tokio::spawn(async move {
@ -65,7 +83,7 @@ pub(crate) async fn handle_media(
.execute(&mut **t) .execute(&mut **t)
.await?; .await?;
spawn_media_action("download", media_id, from_user_id, media.sender_message_id); spawn_media_download(media_id);
return Ok(()); return Ok(());
} }
@ -122,7 +140,7 @@ pub(crate) async fn handle_media(
.execute(&mut **t) .execute(&mut **t)
.await?; .await?;
spawn_media_action("download", media_id, from_user_id, media.sender_message_id); spawn_media_download(media_id);
return Ok(()); return Ok(());
} }
@ -178,7 +196,9 @@ pub(crate) async fn handle_media(
) )
.execute(&mut **t) .execute(&mut **t)
.await?; .await?;
Group::increase_last_message_exchange(t, group_id, timestamp).await?; Group::increase_last_message_exchange(t, group_id, timestamp).await?;
if let Ok(callbacks) = get_callbacks() { if let Ok(callbacks) = get_callbacks() {
let group_id = group_id.to_owned(); let group_id = group_id.to_owned();
let timestamp = media.timestamp; let timestamp = media.timestamp;
@ -186,7 +206,9 @@ pub(crate) async fn handle_media(
(callbacks.api.media_received)(group_id, timestamp).await; (callbacks.api.media_received)(group_id, timestamp).await;
}); });
} }
spawn_media_action("download", media_id, from_user_id, media.sender_message_id);
spawn_media_download(media_id);
Ok(()) Ok(())
} }

View file

@ -4,13 +4,12 @@
*/ */
use super::handle_encrypted; use super::handle_encrypted;
use crate::api::proto::client::{self as proto, encrypted_content}; use crate::api::proto::client::{self as proto};
use crate::api::Server; use crate::api::Server;
use crate::bridge::api::ServerResult; use crate::bridge::api::ServerResult;
use crate::bridge::callbacks::get_callbacks; use crate::bridge::callbacks::get_callbacks;
use crate::context::Context; use crate::context::Context;
use crate::database::app::tables::{Contact, NewReceipt, Receipt}; use crate::database::app::tables::{Contact, MediaFile, NewReceipt, Receipt};
use crate::error::{twonly_error, Result, TwonlyError}; use crate::error::{twonly_error, Result, TwonlyError};
use crate::utils::new_uuid_v4; use crate::utils::new_uuid_v4;
use prost::Message as ProstMessage; use prost::Message as ProstMessage;
@ -25,26 +24,6 @@ use std::{collections::HashMap, sync::LazyLock};
static ALREADY_QUEUED_RECEIPTS: LazyLock<std::sync::Mutex<HashMap<String, std::time::Instant>>> = static ALREADY_QUEUED_RECEIPTS: LazyLock<std::sync::Mutex<HashMap<String, std::time::Instant>>> =
LazyLock::new(|| std::sync::Mutex::new(HashMap::new())); LazyLock::new(|| std::sync::Mutex::new(HashMap::new()));
async fn native_push_data(
_ctx: &Context,
contact_id: i64,
message_id: Option<String>,
plaintext: &[u8],
message_type: i32,
) -> Result<Option<Vec<u8>>> {
let callbacks = match get_callbacks() {
Ok(callbacks) => callbacks,
Err(TwonlyError::MissingCallbackInitialization) => {
return Ok(None);
}
Err(error) => return Err(error),
};
Ok(
(callbacks.api.create_push_data)(contact_id, message_id, plaintext.to_vec(), message_type)
.await,
)
}
pub(crate) async fn queue_encrypted_content( pub(crate) async fn queue_encrypted_content(
transaction: &mut Transaction<'_, Sqlite>, transaction: &mut Transaction<'_, Sqlite>,
target_user_id: i64, target_user_id: i64,
@ -103,7 +82,7 @@ pub(crate) async fn process_encrypted_or_queue_error(
let Some(error_type) = error_type else { let Some(error_type) = error_type else {
return Err(error); return Err(error);
}; };
let outgoing_receipt_id = uuid::Uuid::new_v4().to_string(); let outgoing_receipt_id = new_uuid_v4();
let response_content = proto::EncryptedContent { let response_content = proto::EncryptedContent {
group_id, group_id,
error_messages: Some(proto::encrypted_content::ErrorMessages { error_messages: Some(proto::encrypted_content::ErrorMessages {
@ -318,18 +297,7 @@ 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 message_type = proto::message::Type::try_from(message.r#type)?;
let push_data = if row.retry_count == 0 { let push_data: Option<Vec<u8>> = None;
native_push_data(
ctx,
row.contact_id,
row.message_id.clone(),
&message.encrypted_content.clone().unwrap_or_default(),
message.r#type,
)
.await?
} else {
None
};
match message_type { match message_type {
proto::message::Type::Ciphertext | proto::message::Type::PrekeyBundle => { proto::message::Type::Ciphertext | proto::message::Type::PrekeyBundle => {
@ -422,18 +390,7 @@ pub(crate) async fn prepare_queued_receipt(
let mut message = proto::Message::decode(row.message.as_slice()) let mut message = proto::Message::decode(row.message.as_slice())
.map_err(|error| TwonlyError::Generic(format!("invalid queued message: {error}")))?; .map_err(|error| TwonlyError::Generic(format!("invalid queued message: {error}")))?;
message.receipt_id = receipt_id.to_owned(); message.receipt_id = receipt_id.to_owned();
let push_data = if row.retry_count == 0 { let push_data: Option<Vec<u8>> = None;
native_push_data(
ctx,
row.contact_id,
row.message_id,
&message.encrypted_content.clone().unwrap_or_default(),
message.r#type,
)
.await?
} else {
None
};
match proto::message::Type::try_from(message.r#type) match proto::message::Type::try_from(message.r#type)
.map_err(|_| TwonlyError::Generic("queued message has invalid type".into()))? .map_err(|_| TwonlyError::Generic("queued message has invalid type".into()))?
@ -593,6 +550,8 @@ pub(crate) async fn handle_sender_delivery_receipt(
) )
.execute(&mut **transaction) .execute(&mut **transaction)
.await?; .await?;
MediaFile::handle_response_from_receiver(transaction, &message_id).await?;
} }
sqlx::query!( sqlx::query!(
r#" r#"
@ -635,144 +594,3 @@ pub async fn handle_plaintext_content(
} }
Ok(None) Ok(None)
} }
pub(crate) async fn handle_message_update(
transaction: &mut Transaction<'_, Sqlite>,
from_user_id: i64,
update: encrypted_content::MessageUpdate,
) -> Result<()> {
use encrypted_content::message_update::Type;
let timestamp = crate::utils::milliseconds_to_seconds(update.timestamp);
match Type::try_from(update.r#type)
.map_err(|_| TwonlyError::Generic("invalid message update".into()))?
{
Type::Opened => {
for message_id in update.multiple_target_message_ids {
let action_at = sqlx::query_scalar::<_, i64>(
"SELECT MAX(created_at, ?) FROM messages WHERE message_id = ?",
)
.bind(timestamp)
.bind(&message_id)
.fetch_optional(&mut **transaction)
.await?;
let Some(action_at) = action_at else { continue };
sqlx::query!(
r#"
INSERT INTO message_actions(message_id, contact_id, type, action_at)
VALUES (?, ?, 'openedAt', ?)
ON CONFLICT(message_id, contact_id, type)
DO UPDATE SET action_at = excluded.action_at
"#,
message_id,
from_user_id,
action_at,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
r#"UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS(
SELECT 1 FROM group_members gm
WHERE gm.group_id = messages.group_id AND NOT EXISTS(
SELECT 1 FROM message_actions ma
WHERE ma.message_id = messages.message_id
AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt'
)
) THEN ? ELSE NULL END
WHERE message_id = ?"#,
action_at,
action_at,
message_id,
)
.execute(&mut **transaction)
.await?;
}
}
Type::Delete => {
let media_id = sqlx::query_scalar!(
"SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?",
update.sender_message_id,
from_user_id,
)
.fetch_optional(&mut **transaction)
.await?
.flatten();
sqlx::query!(
"DELETE FROM message_histories WHERE message_id = ?",
update.sender_message_id
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"DELETE FROM receipts WHERE message_id = ?",
update.sender_message_id
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
r#"
UPDATE messages
SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ?
WHERE message_id = ? AND sender_id = ?
"#,
timestamp,
update.sender_message_id,
from_user_id,
)
.execute(&mut **transaction)
.await?;
if let Some(media_id) = media_id {
let references = sqlx::query_scalar!(
"SELECT COUNT(*) FROM messages WHERE media_id = ?",
media_id,
)
.fetch_one(&mut **transaction)
.await?;
if references == 0 {
sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id)
.execute(&mut **transaction)
.await?;
if let Ok(callbacks) = get_callbacks() {
if let Some(message_id) = update.sender_message_id.clone() {
tokio::spawn(async move {
(callbacks.api.media_action)(
"delete".into(),
media_id,
from_user_id,
message_id,
)
.await;
});
}
}
}
}
}
Type::EditText => {
sqlx::query!(
r#"INSERT INTO message_histories(message_id, content, created_at)
SELECT message_id, content, ? FROM messages
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL"#,
timestamp,
update.sender_message_id,
from_user_id,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
r#"
UPDATE messages
SET content = ?, modified_at = ?
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL
"#,
update.text,
timestamp,
update.sender_message_id,
from_user_id,
)
.execute(&mut **transaction)
.await?;
}
}
Ok(())
}

View file

@ -9,21 +9,17 @@ use crate::context::Context;
use crate::database::app::tables::{Contact, Group, Receipt}; use crate::database::app::tables::{Contact, Group, Receipt};
use crate::error::{Result, TwonlyError}; use crate::error::{Result, TwonlyError};
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
use std::collections::HashMap; use std::sync::Arc;
use std::sync::{Arc, Mutex, OnceLock};
use typing_indicator::handle_typing_indicator; use typing_indicator::handle_typing_indicator;
static REQUESTED_PROFILES: OnceLock<Mutex<HashMap<i64, i64>>> = OnceLock::new();
mod additional_data; mod additional_data;
pub(crate) mod contact; pub(crate) mod contact;
mod errors; mod errors;
mod groups; mod groups;
mod media; mod media;
pub mod messages; pub mod messages;
mod pushkeys;
mod reaction; mod reaction;
mod recovery; pub(crate) mod recovery;
mod text_message; mod text_message;
mod typing_indicator; mod typing_indicator;
mod user_discovery; mod user_discovery;
@ -34,64 +30,21 @@ mod verification;
/// this dispatcher. /// this dispatcher.
pub(crate) async fn handle_encrypted( pub(crate) async fn handle_encrypted(
ctx: &Arc<Context>, ctx: &Arc<Context>,
tr: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
receipt_id: &str, receipt_id: &str,
content: proto::EncryptedContent, content: proto::EncryptedContent,
) -> Result<()> { ) -> Result<()> {
Receipt::mark_all_for_retry(tr, from_user_id).await?; Receipt::mark_all_for_retry(t, from_user_id).await?;
if let Some(version) = content.sender_user_discovery_version.clone() { if let Some(version) = content.sender_user_discovery_version.clone() {
user_discovery::check_sender_version(ctx, tr, from_user_id, version).await?; user_discovery::check_sender_version(ctx, t, from_user_id, version).await?;
} }
if let Some(sender_profile_counter) = content.sender_profile_counter { contact::check_for_profile_update(t, from_user_id, &content).await?;
let current_counter = sqlx::query_scalar!(
"SELECT sender_profile_counter FROM contacts WHERE user_id = ?",
from_user_id,
)
.fetch_optional(&mut **tr)
.await?
.unwrap_or(0);
if content.contact_update.is_none() && sender_profile_counter > current_counter {
let should_request = {
let mut requested = REQUESTED_PROFILES
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap();
let last_requested = requested.get(&from_user_id).copied().unwrap_or(0);
if sender_profile_counter > last_requested {
requested.insert(from_user_id, sender_profile_counter);
true
} else {
false
}
};
if should_request {
messages::queue_encrypted_content(
tr,
from_user_id,
proto::EncryptedContent {
contact_update: Some(proto::encrypted_content::ContactUpdate {
r#type: proto::encrypted_content::contact_update::Type::Request as i32,
username: None,
display_name: None,
avatar_svg_compressed: None,
}),
..Default::default()
},
true,
)
.await?;
}
}
}
if content.ask_for_friend_promotions == Some(true) { if content.ask_for_friend_promotions == Some(true) {
Contact::update_ask_for_friend_promotions(tr, from_user_id).await?; Contact::update_ask_for_friend_promotions(t, from_user_id).await?;
} }
let type_kind = content_type_kind(&content); let type_kind = content_type_kind(&content);
@ -100,13 +53,13 @@ pub(crate) async fn handle_encrypted(
tracing::info!("Handling incoming message: {type_kind}"); tracing::info!("Handling incoming message: {type_kind}");
if let Some(request) = content.contact_request { if let Some(request) = content.contact_request {
return contact::handle_contact_request(ctx, tr, from_user_id, request).await; return contact::handle_contact_request(ctx, t, from_user_id, request).await;
} }
if let Some(update) = content.contact_update { if let Some(update) = content.contact_update {
return contact::handle_contact_update( return contact::handle_contact_update(
ctx, ctx,
tr, t,
from_user_id, from_user_id,
content.sender_profile_counter, content.sender_profile_counter,
update, update,
@ -115,46 +68,42 @@ pub(crate) async fn handle_encrypted(
} }
if let Some(update) = content.message_update { if let Some(update) = content.message_update {
return messages::handle_message_update(tr, from_user_id, update).await; return text_message::handle_message_update(t, from_user_id, update).await;
} }
if let Some(update) = content.media_update { if let Some(update) = content.media_update {
return media::handle_media_update(tr, from_user_id, update).await; return media::handle_media_update(t, from_user_id, update).await;
} }
if let Some(error) = content.error_messages { if let Some(error) = content.error_messages {
return errors::handle_error_message( return errors::handle_error_message(
tr, ctx,
t,
from_user_id, from_user_id,
receipt_id,
content.group_id.as_deref(), content.group_id.as_deref(),
error, error,
) )
.await; .await;
} }
if let Some(push_keys) = content.push_keys {
return pushkeys::handle_push_key(tr, from_user_id, push_keys).await;
}
if let Some(update) = content.user_discovery_update { if let Some(update) = content.user_discovery_update {
return user_discovery::handle_user_discovery_update(ctx, from_user_id, update).await; return user_discovery::handle_user_discovery_update(ctx, t, from_user_id, update).await;
} }
if let Some(request) = content.user_discovery_request { if let Some(request) = content.user_discovery_request {
return user_discovery::handle_user_discovery_request(ctx, tr, from_user_id, request).await; return user_discovery::handle_user_discovery_request(ctx, t, from_user_id, request).await;
} }
if let Some(proof) = content.key_verification_proof { if let Some(proof) = content.key_verification_proof {
return verification::handle_key_verification_proof(tr, from_user_id, proof).await; return verification::handle_key_verification_proof(t, from_user_id, proof).await;
} }
if let Some(recovery) = content.passwordless_recovery { if let Some(recovery) = content.passwordless_recovery {
return recovery::handle_passwordless_recovery(tr, from_user_id, recovery).await; return recovery::handle_passwordless_recovery(t, from_user_id, recovery).await;
} }
if let Some(heartbeat) = content.passwordless_recovery_heartbeat { if let Some(heartbeat) = content.passwordless_recovery_heartbeat {
return recovery::handle_passwordless_recovery_heartbeat(tr, from_user_id, heartbeat).await; return recovery::handle_passwordless_recovery_heartbeat(t, from_user_id, heartbeat).await;
} }
let group_id = content let group_id = content
@ -162,11 +111,11 @@ pub(crate) async fn handle_encrypted(
.ok_or_else(|| TwonlyError::Generic("group-scoped message has no group ID".into()))?; .ok_or_else(|| TwonlyError::Generic("group-scoped message has no group ID".into()))?;
if let Some(create) = content.group_create { if let Some(create) = content.group_create {
return groups::handle_group_create(tr, from_user_id, &group_id, create).await; return groups::handle_group_create(ctx, t, from_user_id, &group_id, create).await;
} }
if let Some(join) = content.group_join { if let Some(join) = content.group_join {
return groups::handle_group_join(tr, from_user_id, &group_id, join).await; return groups::handle_group_join(t, from_user_id, &group_id, join).await;
} }
let is_member = sqlx::query_scalar!( let is_member = sqlx::query_scalar!(
@ -174,7 +123,7 @@ pub(crate) async fn handle_encrypted(
group_id, group_id,
from_user_id, from_user_id,
) )
.fetch_one(&mut **tr) .fetch_one(&mut **t)
.await? .await?
!= 0; != 0;
@ -182,21 +131,21 @@ pub(crate) async fn handle_encrypted(
let local_user_id = ctx.user_id().await?; let local_user_id = ctx.user_id().await?;
if Group::direct_chat_id(local_user_id, from_user_id) == group_id { if Group::direct_chat_id(local_user_id, from_user_id) == group_id {
let contact = Contact::get_contact_by_id(tr, from_user_id).await?; let contact = Contact::get_contact_by_id(t, from_user_id).await?;
if let Some(contact) = if let Some(contact) =
contact.filter(|value| value.accepted != 0 && value.deleted_by_user == 0) contact.filter(|value| value.accepted != 0 && value.deleted_by_user == 0)
{ {
Group::create_direct_chat(ctx, tr, contact).await?; Group::create_direct_chat(ctx, t, contact).await?;
} else { } else {
sqlx::query!( sqlx::query!(
"UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?", "UPDATE contacts SET requested = 1, deleted_by_user = 0 WHERE user_id = ?",
from_user_id, from_user_id,
) )
.execute(&mut **tr) .execute(&mut **t)
.await?; .await?;
messages::queue_encrypted_content( messages::queue_encrypted_content(
tr, t,
from_user_id, from_user_id,
proto::EncryptedContent { proto::EncryptedContent {
error_messages: Some(proto::encrypted_content::ErrorMessages { error_messages: Some(proto::encrypted_content::ErrorMessages {
@ -213,28 +162,28 @@ pub(crate) async fn handle_encrypted(
} }
} }
groups::ensure_group_member(tr, from_user_id, &group_id).await?; groups::ensure_group_member(t, from_user_id, &group_id).await?;
if content.resend_group_public_key.is_some() { if content.resend_group_public_key.is_some() {
return groups::handle_resend_group_public_key(tr, from_user_id, &group_id).await; return groups::handle_resend_group_public_key(t, from_user_id, &group_id).await;
} }
if let Some(update) = content.group_update { if let Some(update) = content.group_update {
return groups::handle_group_update(tr, from_user_id, &group_id, update).await; return groups::handle_group_update(ctx, t, from_user_id, &group_id, update).await;
} }
if let Some(flame) = content.flame_sync { if let Some(flame) = content.flame_sync {
return groups::handle_flame_sync(tr, &group_id, flame).await; return groups::handle_flame_sync(t, &group_id, flame).await;
} }
if let Some(message) = content.text_message { if let Some(message) = content.text_message {
return text_message::handle_text_message(tr, from_user_id, &group_id, message).await; return text_message::handle_text_message(t, from_user_id, &group_id, message).await;
} }
if let Some(message) = content.additional_data_message { if let Some(message) = content.additional_data_message {
return additional_data::handle_additional_data_message( return additional_data::handle_additional_data_message(
ctx, ctx,
tr, t,
from_user_id, from_user_id,
&group_id, &group_id,
message, message,
@ -243,15 +192,15 @@ pub(crate) async fn handle_encrypted(
} }
if let Some(media) = content.media { if let Some(media) = content.media {
return media::handle_media(tr, from_user_id, &group_id, media).await; return media::handle_media(t, from_user_id, &group_id, media).await;
} }
if let Some(reaction) = content.reaction { if let Some(reaction) = content.reaction {
return reaction::handle_reaction(tr, from_user_id, &group_id, reaction).await; return reaction::handle_reaction(t, from_user_id, &group_id, reaction).await;
} }
if let Some(indicator) = content.typing_indicator { if let Some(indicator) = content.typing_indicator {
return handle_typing_indicator(tr, from_user_id, &group_id, indicator).await; return handle_typing_indicator(t, from_user_id, &group_id, indicator).await;
} }
Err(TwonlyError::Generic(format!( Err(TwonlyError::Generic(format!(

View file

@ -1,65 +0,0 @@
/*
* Copyright (c) 2026, Tobias Müller git@tsmr.eu
*
*/
use crate::api::proto::client::encrypted_content;
use crate::error::{Result, TwonlyError};
use crate::utils::milliseconds_to_seconds;
use sqlx::{Sqlite, Transaction};
use std::sync::atomic::{AtomicI64, Ordering};
static LAST_PUSH_KEY_REQUEST: AtomicI64 = AtomicI64::new(0);
pub(crate) async fn handle_push_key(
tx: &mut Transaction<'_, Sqlite>,
user: i64,
value: encrypted_content::PushKeys,
) -> Result<()> {
use encrypted_content::push_keys::Type;
match Type::try_from(value.r#type)
.map_err(|_| TwonlyError::Generic("invalid push-key message".into()))?
{
Type::Update => {
let (Some(id), Some(key), Some(created_at)) =
(value.key_id, value.key, value.created_at)
else {
return Err(TwonlyError::Generic("incomplete push-key update".into()));
};
sqlx::query!(
r#"
INSERT INTO contact_push_keys(contact_id, key_id, key, created_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(contact_id, key_id)
DO UPDATE SET key = excluded.key, created_at = excluded.created_at
"#,
user,
id,
key,
milliseconds_to_seconds(created_at),
)
.execute(&mut **tx)
.await?;
Ok(())
}
Type::Request => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let previous = LAST_PUSH_KEY_REQUEST.load(Ordering::Relaxed);
if now - previous < 60
|| LAST_PUSH_KEY_REQUEST
.compare_exchange(previous, now, Ordering::AcqRel, Ordering::Relaxed)
.is_err()
{
return Ok(());
}
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
tokio::spawn(async move {
(callbacks.api.push_key_requested)(user).await;
});
}
Ok(())
}
}
}

View file

@ -4,8 +4,7 @@
*/ */
use crate::api::proto::client::encrypted_content; use crate::api::proto::client::encrypted_content;
use crate::bridge::callbacks::get_callbacks; use crate::database::app::tables::{Group, MediaFile, Receipt};
use crate::database::app::tables::Group;
use crate::error::Result; use crate::error::Result;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
@ -16,29 +15,20 @@ pub(crate) async fn handle_reaction(
reaction: encrypted_content::Reaction, reaction: encrypted_content::Reaction,
) -> Result<()> { ) -> Result<()> {
if reaction.remove { if reaction.remove {
sqlx::query!( Receipt::delete_reaction(
r#" t,
DELETE FROM reactions &reaction.target_message_id,
WHERE message_id = ? AND sender_id = ? AND emoji = ?
"#,
reaction.target_message_id,
from_user_id, from_user_id,
reaction.emoji, &reaction.emoji,
) )
.execute(&mut **t)
.await?; .await?;
} else { } else {
sqlx::query!( Receipt::insert_reaction(
r#" t,
INSERT INTO reactions(message_id, emoji, sender_id) &reaction.target_message_id,
VALUES (?, ?, ?)
ON CONFLICT(message_id, sender_id, emoji) DO NOTHING
"#,
reaction.target_message_id,
reaction.emoji,
from_user_id, from_user_id,
&reaction.emoji,
) )
.execute(&mut **t)
.await?; .await?;
} }
@ -46,18 +36,7 @@ pub(crate) async fn handle_reaction(
Group::increase_last_message_exchange_to_now(t, group_id).await?; Group::increase_last_message_exchange_to_now(t, group_id).await?;
} }
if let Ok(callbacks) = get_callbacks() { MediaFile::handle_response_from_receiver(t, &reaction.target_message_id).await?;
let message_id = reaction.target_message_id;
tokio::spawn(async move {
(callbacks.api.media_action)(
"response".into(),
String::new(),
from_user_id,
message_id,
)
.await;
});
}
Ok(()) Ok(())
} }

View file

@ -3,13 +3,21 @@
* *
*/ */
use crate::api::messages::outgoing::send_c2c_message_to_contact;
use crate::api::proto::client::encrypted_content; use crate::api::proto::client::encrypted_content;
use crate::error::Result; use crate::api::proto::client::EncryptedContent;
use crate::api::Server;
use crate::bridge::api::ServerResult;
use crate::context::Context;
use crate::error::{twonly_error, Result};
use crate::user_config::UserConfig;
use prost::Message as _;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
use std::sync::Arc;
pub(crate) async fn handle_passwordless_recovery( pub(crate) async fn handle_passwordless_recovery(
transaction: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
recovery: encrypted_content::PasswordLessRecovery, recovery: encrypted_content::PasswordLessRecovery,
) -> Result<()> { ) -> Result<()> {
@ -24,7 +32,7 @@ pub(crate) async fn handle_passwordless_recovery(
"#, "#,
from_user_id, from_user_id,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
} else if let Some(share) = recovery.recovery_secret_share { } else if let Some(share) = recovery.recovery_secret_share {
sqlx::query!( sqlx::query!(
@ -39,19 +47,137 @@ pub(crate) async fn handle_passwordless_recovery(
recovery.threshold, recovery.threshold,
from_user_id, from_user_id,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
} }
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() { #[cfg(not(test))]
{
let ctx = Context::get_static()?.clone();
tokio::spawn(async move { tokio::spawn(async move {
(callbacks.api.recovery_changed)().await; if let Err(error) = perform_heartbeat(&ctx).await {
tracing::warn!(%error, "passwordless recovery heartbeat failed");
}
}); });
} }
Ok(()) Ok(())
} }
pub(crate) async fn perform_heartbeat(ctx: &Arc<Context>) -> Result<()> {
let now = chrono::Utc::now();
let base_config = UserConfig::load_required_from(ctx)?;
let mut config = base_config.clone();
if let Some(recovery) = config.password_less_recovery.as_mut() {
let server_due = recovery
.last_server_heartbeat
.is_none_or(|last| now.signed_duration_since(last).num_days() > 20);
if server_due {
if let Some(encrypted_key) = recovery.encrypted_server_key.clone() {
if let ServerResult::ErrorCode(_) = Server::register_passwordless_recovery(
ctx,
encrypted_key,
recovery.pin_unlock_token.clone(),
)
.await?
{
return Err(twonly_error!("passwordless registration failed: {code}"));
}
recovery.last_server_heartbeat = Some(now);
}
}
let contacts_due = recovery
.last_contact_heartbeat
.is_none_or(|last| now.signed_duration_since(last).num_hours() >= 24);
if contacts_due {
let database = ctx.get_app_database().await;
let contacts = sqlx::query!(
r#"SELECT user_id, recovery_secret_share FROM contacts
WHERE recovery_is_trusted_friend = 1
AND recovery_last_heartbeat IS NULL
AND recovery_secret_share IS NOT NULL"#
)
.fetch_all(&database.pool)
.await?;
drop(database);
for contact in contacts {
let content = EncryptedContent {
passwordless_recovery: Some(encrypted_content::PasswordLessRecovery {
recovery_secret_share: contact.recovery_secret_share,
threshold: recovery.threshold,
delete: false,
}),
..Default::default()
};
send_c2c_message_to_contact()
.ctx(ctx)
.contact_id(contact.user_id)
.encrypted_content(content.encode_to_vec())
.call()
.await?;
}
recovery.last_contact_heartbeat = Some(now);
}
}
let database = ctx.get_app_database().await;
let contacts = sqlx::query!(
r#"SELECT user_id, recovery_contacts_secret_share FROM contacts
WHERE recovery_contacts_secret_share IS NOT NULL
AND (recovery_contacts_last_heartbeat IS NULL
OR recovery_contacts_last_heartbeat <= ?)"#,
(now - chrono::Duration::days(7)).timestamp(),
)
.fetch_all(&database.pool)
.await?;
drop(database);
for contact in contacts {
let Some(share) = contact.recovery_contacts_secret_share else {
continue;
};
let content = EncryptedContent {
passwordless_recovery_heartbeat: Some(
encrypted_content::PasswordLessRecoveryHeartbeat {
hash: Sha256::digest(share).to_vec(),
},
),
..Default::default()
};
send_c2c_message_to_contact()
.ctx(ctx)
.contact_id(contact.user_id)
.encrypted_content(content.encode_to_vec())
.call()
.await?;
let database = ctx.get_app_database().await;
sqlx::query!(
"UPDATE contacts SET recovery_contacts_last_heartbeat = ? WHERE user_id = ?",
now.timestamp(),
contact.user_id,
)
.execute(&database.pool)
.await?;
}
if config != base_config {
UserConfig::update_json(
ctx,
&serde_json::to_string(&base_config)?,
&serde_json::to_string(&config)?,
)?;
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
(callbacks.api.user_config_changed)(config).await;
}
}
Ok(())
}
pub(crate) async fn handle_passwordless_recovery_heartbeat( pub(crate) async fn handle_passwordless_recovery_heartbeat(
transaction: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
heartbeat: encrypted_content::PasswordLessRecoveryHeartbeat, heartbeat: encrypted_content::PasswordLessRecoveryHeartbeat,
) -> Result<()> { ) -> Result<()> {
@ -63,15 +189,17 @@ pub(crate) async fn handle_passwordless_recovery_heartbeat(
"#, "#,
from_user_id, from_user_id,
) )
.fetch_optional(&mut **transaction) .fetch_optional(&mut **t)
.await? .await?
.flatten(); .flatten();
let valid = share let valid = share
.as_deref() .as_deref()
.is_some_and(|share| Sha256::digest(share).as_slice() == heartbeat.hash); .is_some_and(|share| Sha256::digest(share).as_slice() == heartbeat.hash);
if share.is_none() { if share.is_none() {
super::messages::queue_encrypted_content( super::messages::queue_encrypted_content(
transaction, t,
from_user_id, from_user_id,
crate::api::proto::client::EncryptedContent { crate::api::proto::client::EncryptedContent {
passwordless_recovery: Some(encrypted_content::PasswordLessRecovery { passwordless_recovery: Some(encrypted_content::PasswordLessRecovery {
@ -97,7 +225,153 @@ pub(crate) async fn handle_passwordless_recovery_heartbeat(
valid, valid,
from_user_id, from_user_id,
) )
.execute(&mut **transaction) .execute(&mut **t)
.await?; .await?;
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
async fn context() -> anyhow::Result<(tempfile::TempDir, Arc<Context>)> {
let temp = tempfile::tempdir()?;
let database_dir = temp.path().join("database");
let data_dir = temp.path().join("data");
std::fs::create_dir_all(data_dir.join("keyvalue"))?;
std::fs::write(
data_dir.join("keyvalue/user.json"),
serde_json::to_vec(&UserConfig::default())?,
)?;
let context = Context::init_for_testing(database_dir, data_dir).await?;
Ok((temp, context))
}
async fn insert_contact(ctx: &Arc<Context>, user_id: i64) -> Result<()> {
let database = ctx.get_app_database().await;
sqlx::query!(
"INSERT INTO contacts(user_id, username, accepted) VALUES (?, ?, 1)",
user_id,
format!("user_{user_id}"),
)
.execute(&database.pool)
.await?;
Ok(())
}
#[tokio::test]
async fn recovery_share_is_stored_and_deleted() -> anyhow::Result<()> {
let (_temp, ctx) = context().await?;
insert_contact(&ctx, 7).await?;
let database = ctx.get_app_database().await;
let mut transaction = database.pool.begin().await?;
handle_passwordless_recovery(
&mut transaction,
7,
encrypted_content::PasswordLessRecovery {
recovery_secret_share: Some(vec![1, 2, 3]),
threshold: 2,
delete: false,
},
)
.await?;
transaction.commit().await?;
let stored = sqlx::query!(
"SELECT recovery_contacts_secret_share, recovery_contacts_threshold FROM contacts WHERE user_id = 7"
)
.fetch_one(&database.pool)
.await?;
assert_eq!(stored.recovery_contacts_secret_share, Some(vec![1, 2, 3]));
assert_eq!(stored.recovery_contacts_threshold, Some(2));
let mut transaction = database.pool.begin().await?;
handle_passwordless_recovery(
&mut transaction,
7,
encrypted_content::PasswordLessRecovery {
delete: true,
..Default::default()
},
)
.await?;
transaction.commit().await?;
let deleted = sqlx::query!(
"SELECT recovery_contacts_secret_share, recovery_contacts_threshold FROM contacts WHERE user_id = 7"
)
.fetch_one(&database.pool)
.await?;
assert_eq!(deleted.recovery_contacts_secret_share, None);
assert_eq!(deleted.recovery_contacts_threshold, None);
Ok(())
}
#[tokio::test]
async fn valid_and_invalid_heartbeat_update_the_expected_state() -> anyhow::Result<()> {
let (_temp, ctx) = context().await?;
insert_contact(&ctx, 8).await?;
let database = ctx.get_app_database().await;
let share = vec![4, 5, 6];
sqlx::query!(
"UPDATE contacts SET recovery_secret_share = ? WHERE user_id = 8",
share,
)
.execute(&database.pool)
.await?;
let mut transaction = database.pool.begin().await?;
handle_passwordless_recovery_heartbeat(
&mut transaction,
8,
encrypted_content::PasswordLessRecoveryHeartbeat {
hash: Sha256::digest([4, 5, 6]).to_vec(),
},
)
.await?;
transaction.commit().await?;
let valid =
sqlx::query_scalar!("SELECT recovery_last_heartbeat FROM contacts WHERE user_id = 8")
.fetch_one(&database.pool)
.await?;
assert!(valid.is_some());
let mut transaction = database.pool.begin().await?;
handle_passwordless_recovery_heartbeat(
&mut transaction,
8,
encrypted_content::PasswordLessRecoveryHeartbeat { hash: vec![0; 32] },
)
.await?;
transaction.commit().await?;
let invalid =
sqlx::query_scalar!("SELECT recovery_last_heartbeat FROM contacts WHERE user_id = 8")
.fetch_one(&database.pool)
.await?;
assert_eq!(invalid, None);
Ok(())
}
#[tokio::test]
async fn heartbeat_without_share_queues_deletion_response() -> anyhow::Result<()> {
let (_temp, ctx) = context().await?;
insert_contact(&ctx, 9).await?;
let database = ctx.get_app_database().await;
let mut transaction = database.pool.begin().await?;
handle_passwordless_recovery_heartbeat(
&mut transaction,
9,
encrypted_content::PasswordLessRecoveryHeartbeat { hash: vec![1; 32] },
)
.await?;
transaction.commit().await?;
let queued = sqlx::query_scalar!("SELECT COUNT(*) FROM receipts WHERE contact_id = 9")
.fetch_one(&database.pool)
.await?;
assert_eq!(queued, 1);
Ok(())
}
}

View file

@ -7,6 +7,7 @@ use crate::api::proto::client::encrypted_content;
use crate::database::app::tables::{Group, Message, MessageType, NewMessage}; use crate::database::app::tables::{Group, Message, MessageType, NewMessage};
use crate::error::Result; use crate::error::Result;
use crate::utils::milliseconds_to_seconds; use crate::utils::milliseconds_to_seconds;
use encrypted_content::message_update::Type;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
pub(crate) async fn handle_text_message( pub(crate) async fn handle_text_message(
@ -36,3 +37,162 @@ pub(crate) async fn handle_text_message(
Ok(()) Ok(())
} }
pub(crate) async fn handle_message_update(
t: &mut Transaction<'_, Sqlite>,
from_user_id: i64,
update: encrypted_content::MessageUpdate,
) -> Result<()> {
let timestamp = milliseconds_to_seconds(update.timestamp);
let update_type = Type::try_from(update.r#type)?;
tracing::info!(?update_type, from_user_id, "update text message");
match update_type {
Type::Opened => {
for message_id in update.multiple_target_message_ids {
let action_at = sqlx::query_scalar::<_, i64>(
"SELECT MAX(created_at, ?) FROM messages WHERE message_id = ?",
)
.bind(timestamp)
.bind(&message_id)
.fetch_optional(&mut **t)
.await?;
let Some(action_at) = action_at else { continue };
sqlx::query!(
r#"
INSERT INTO message_actions(message_id, contact_id, type, action_at)
VALUES (?, ?, 'openedAt', ?)
ON CONFLICT(message_id, contact_id, type)
DO UPDATE SET action_at = excluded.action_at
"#,
message_id,
from_user_id,
action_at,
)
.execute(&mut **t)
.await?;
sqlx::query!(
r#"UPDATE messages SET opened_at = ?, opened_by_all = CASE WHEN NOT EXISTS(
SELECT 1 FROM group_members gm
WHERE gm.group_id = messages.group_id AND NOT EXISTS(
SELECT 1 FROM message_actions ma
WHERE ma.message_id = messages.message_id
AND ma.contact_id = gm.contact_id AND ma.type = 'openedAt'
)
) THEN ? ELSE NULL END
WHERE message_id = ?"#,
action_at,
action_at,
message_id,
)
.execute(&mut **t)
.await?;
}
}
Type::Delete => {
let media_id = sqlx::query_scalar!(
"SELECT media_id FROM messages WHERE message_id = ? AND sender_id = ?",
update.sender_message_id,
from_user_id,
)
.fetch_optional(&mut **t)
.await?
.flatten();
sqlx::query!(
"DELETE FROM message_histories WHERE message_id = ?",
update.sender_message_id
)
.execute(&mut **t)
.await?;
sqlx::query!(
"DELETE FROM receipts WHERE message_id = ?",
update.sender_message_id
)
.execute(&mut **t)
.await?;
sqlx::query!(
r#"
UPDATE messages
SET is_deleted_from_sender = 1, content = NULL, media_id = NULL, modified_at = ?
WHERE message_id = ? AND sender_id = ?
"#,
timestamp,
update.sender_message_id,
from_user_id,
)
.execute(&mut **t)
.await?;
if let Some(media_id) = media_id {
let references = sqlx::query_scalar!(
"SELECT COUNT(*) FROM messages WHERE media_id = ?",
media_id,
)
.fetch_one(&mut **t)
.await?;
if references == 0 {
let media_type = sqlx::query_scalar!(
"SELECT type FROM media_files WHERE media_id = ?",
media_id,
)
.fetch_optional(&mut **t)
.await?;
sqlx::query!("DELETE FROM media_files WHERE media_id = ?", media_id)
.execute(&mut **t)
.await?;
if let Some(media_type) = media_type {
let ctx = crate::context::Context::get_static()?;
let ctx = ctx.clone();
tokio::spawn(async move {
// Let the surrounding message transaction commit before
// applying its corresponding filesystem side effect.
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
if let Err(error) =
crate::services::mediafiles::MediaFileService::new(&ctx)
.remove_files_if_deleted(&media_id, &media_type)
.await
{
tracing::warn!(media_id, %error, "could not remove media files");
}
});
}
}
}
}
Type::EditText => {
sqlx::query!(
r#"INSERT INTO message_histories(message_id, content, created_at)
SELECT message_id, content, ? FROM messages
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL"#,
timestamp,
update.sender_message_id,
from_user_id,
)
.execute(&mut **t)
.await?;
sqlx::query!(
r#"
UPDATE messages
SET content = ?, modified_at = ?
WHERE message_id = ? AND sender_id = ? AND content IS NOT NULL
"#,
update.text,
timestamp,
update.sender_message_id,
from_user_id,
)
.execute(&mut **t)
.await?;
}
}
Ok(())
}

View file

@ -9,13 +9,10 @@ use crate::context::Context;
use crate::database::app::tables::Contact; use crate::database::app::tables::Contact;
use crate::error::{twonly_error, Result, TwonlyError}; use crate::error::{twonly_error, Result, TwonlyError};
use crate::user_config::UserConfig; use crate::user_config::UserConfig;
use crate::user_discovery::UserDiscoveryVersion;
use prost::Message;
use sqlx::{Sqlite, Transaction}; use sqlx::{Sqlite, Transaction};
use std::collections::HashSet; use std::sync::Arc;
use std::sync::LazyLock;
use tokio::sync::Mutex;
static REQUESTED_UPDATES: LazyLock<Mutex<HashSet<i64>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
pub(crate) async fn check_sender_version( pub(crate) async fn check_sender_version(
ctx: &Context, ctx: &Context,
@ -33,20 +30,30 @@ pub(crate) async fn check_sender_version(
)); ));
} }
let Some(current_version) = ctx // The inbound message transaction owns the app database's sole connection.
.get_user_discovery() // Going through `UserDiscovery::should_request_new_messages` here would ask
.get() // the native store to acquire that same connection and deadlock until the
.await // pool's 30-second acquire timeout expires.
.should_request_new_messages(from_user_id, &version) let received_version = UserDiscoveryVersion::decode(version.as_slice())?;
.await? let stored_version = sqlx::query_scalar!(
else { "SELECT user_discovery_version FROM contacts WHERE user_id = ?",
return Ok(()); from_user_id,
}; )
.fetch_optional(&mut **t)
.await?
.flatten()
.map(|version| UserDiscoveryVersion::decode(version.as_slice()))
.transpose()?
.unwrap_or_default();
if !REQUESTED_UPDATES.lock().await.insert(from_user_id) { if received_version.announcement <= stored_version.announcement
&& received_version.promotion <= stored_version.promotion
{
return Ok(()); return Ok(());
} }
let current_version = stored_version.encode_to_vec();
queue_encrypted_content( queue_encrypted_content(
t, t,
from_user_id, from_user_id,
@ -62,8 +69,9 @@ pub(crate) async fn check_sender_version(
Ok(()) Ok(())
} }
pub(crate) async fn handle_user_discovery_request( pub(crate) async fn handle_user_discovery_request(
ctx: &Context, ctx: &Arc<Context>,
t: &mut Transaction<'_, Sqlite>, t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
request: encrypted_content::UserDiscoveryRequest, request: encrypted_content::UserDiscoveryRequest,
@ -82,7 +90,7 @@ pub(crate) async fn handle_user_discovery_request(
.get_user_discovery() .get_user_discovery()
.get() .get()
.await .await
.get_new_messages(from_user_id, &request.current_version) .get_new_messages(from_user_id, &request.current_version, t)
.await?; .await?;
if !messages.is_empty() { if !messages.is_empty() {
queue_encrypted_content( queue_encrypted_content(
@ -98,23 +106,27 @@ pub(crate) async fn handle_user_discovery_request(
} }
Ok(()) Ok(())
} }
pub(crate) async fn handle_user_discovery_update( pub(crate) async fn handle_user_discovery_update(
ctx: &Context, ctx: &Arc<Context>,
t: &mut Transaction<'_, Sqlite>,
from_user_id: i64, from_user_id: i64,
update: encrypted_content::UserDiscoveryUpdate, update: encrypted_content::UserDiscoveryUpdate,
) -> Result<()> { ) -> Result<()> {
if !UserConfig::load_required_from(ctx)?.is_user_discovery_enabled { if !UserConfig::load_required_from(ctx)?.is_user_discovery_enabled {
return Ok(()); return Ok(());
} }
if update.messages.iter().any(|message| message.is_empty()) { if update.messages.iter().any(|message| message.is_empty()) {
return Err(TwonlyError::Generic( return Err(TwonlyError::Generic(
"user-discovery update contains an empty message".into(), "user-discovery update contains an empty message".into(),
)); ));
} }
Ok(ctx Ok(ctx
.get_user_discovery() .get_user_discovery()
.get() .get()
.await .await
.handle_new_messages(from_user_id, None, update.messages) .handle_new_messages(from_user_id, None, update.messages, t)
.await?) .await?)
} }

View file

@ -333,6 +333,8 @@ pub(crate) async fn handle_decoded_server_message(
"messages", "messages",
"groups", "groups",
"contacts", "contacts",
"key_verifications",
"user_discovery_own_promotions",
]); ]);
let ctx = ctx.clone(); let ctx = ctx.clone();

View file

@ -12,7 +12,7 @@ use crate::user_config::UserConfig;
use prost::Message as _; use prost::Message as _;
use std::sync::Arc; use std::sync::Arc;
async fn decorate_content( pub(crate) async fn decorate_content(
ctx: &Context, ctx: &Context,
contact_id: i64, contact_id: i64,
content: &mut proto::EncryptedContent, content: &mut proto::EncryptedContent,
@ -35,6 +35,7 @@ async fn decorate_content(
} }
if config.is_user_discovery_enabled && is_persisted_message { if config.is_user_discovery_enabled && is_persisted_message {
ctx.initialize_user_discovery_from_config().await?;
let database = ctx.get_app_database().await; let database = ctx.get_app_database().await;
let allowed = sqlx::query_scalar!( let allowed = sqlx::query_scalar!(
r#"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND accepted = 1 r#"SELECT EXISTS(SELECT 1 FROM contacts WHERE user_id = ? AND accepted = 1

View file

@ -37,7 +37,7 @@ impl ApiAuthHandshaker {
.ok_or_else(|| TwonlyError::Generic("User configuration not found".into()))?; .ok_or_else(|| TwonlyError::Generic("User configuration not found".into()))?;
let device_id = user.device_id; let device_id = user.device_id;
let app_version = user.app_version.to_string(); let app_version = user.app_version.to_string();
Ok((user.user_id, device_id, app_version)) Ok((Some(user.user_id), device_id, app_version))
} }
async fn request_handshake( async fn request_handshake(

View file

@ -9,6 +9,7 @@ use crate::error::Result;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, LazyLock, Weak}; use std::sync::{Arc, LazyLock, Weak};
use std::time::Duration;
use stream_tungstenite::WebSocketClient; use stream_tungstenite::WebSocketClient;
use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; use tokio::sync::{broadcast, oneshot, Mutex, RwLock};
@ -90,6 +91,7 @@ impl ApiClient {
}; };
let client = WebSocketClient::builder(host) let client = WebSocketClient::builder(host)
.receive_timeout(Duration::from_secs(60))
.handshaker(handshaker) .handshaker(handshaker)
.build(); .build();
@ -155,8 +157,11 @@ impl ApiClient {
pub async fn close(&self) { pub async fn close(&self) {
self.deliberately_closed.store(true, Ordering::Release); self.deliberately_closed.store(true, Ordering::Release);
if let Some(_) = self.ws_client.lock().await.take() { let client = self.ws_client.lock().await.take();
// Drop client to disconnect if let Some(client) = client {
if let Err(error) = client.shutdown_graceful(Duration::from_secs(5)).await {
tracing::warn!("WebSocket shutdown did not finish cleanly: {error}");
}
} }
self.fail_pending().await; self.fail_pending().await;
self.set_state(ApiConnectionState::Stopped).await; self.set_state(ApiConnectionState::Stopped).await;

View file

@ -24,31 +24,8 @@ impl Server {
.await .await
} }
pub async fn load_plan_balance(ctx: &Arc<Context>, use_cache: bool) -> Result<Vec<u8>> { pub async fn load_plan_balance(ctx: &Arc<Context>) -> Result<Vec<u8>> {
let database = ctx.get_app_database().await; Self::get_plan_balance(ctx).await
match Self::get_plan_balance(ctx).await {
Ok(response) => {
sqlx::query!(
r#"
INSERT INTO api_state(key, value) VALUES('plan_balance', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value,
updated_at = CAST(strftime('%s', 'now') AS INTEGER)
"#,
response,
)
.execute(&database.pool)
.await?;
database.notify_committed(["api_state"]);
Ok(response)
}
Err(network_error) if use_cache => {
sqlx::query_scalar!("SELECT value FROM api_state WHERE key = 'plan_balance'")
.fetch_optional(&database.pool)
.await?
.ok_or(network_error)
}
Err(error) => Err(error),
}
} }
pub async fn ipa_purchase( pub async fn ipa_purchase(

View file

@ -4,12 +4,15 @@
*/ */
use crate::api::messages::incoming::client2client::messages; use crate::api::messages::incoming::client2client::messages;
use crate::api::proto::server_to_client::response::ok::Ok as ResponseOk;
use crate::api::runtime::helpers::decode_ok_value;
use crate::api::ApiRuntime; use crate::api::ApiRuntime;
pub use crate::api::PqcPreKeyInput; pub use crate::api::PqcPreKeyInput;
use crate::api::Server; use crate::api::Server;
use crate::context::Context; use crate::context::Context;
use crate::error::Result; use crate::error::{Result, TwonlyError};
use crate::frb_generated::StreamSink; use crate::frb_generated::StreamSink;
use crate::services::contacts::ContactService;
use crate::user_config::UserConfig; use crate::user_config::UserConfig;
use flutter_rust_bridge::frb; use flutter_rust_bridge::frb;
use prost::Message; use prost::Message;
@ -20,49 +23,25 @@ pub enum ServerResult<T> {
ErrorCode(i32), ErrorCode(i32),
} }
#[frb] fn api_result<T>(result: ServerResult<T>) -> Result<T> {
pub enum ServerResultEmpty { match result {
Ok, ServerResult::Ok(value) => Ok(value),
ErrorCode(i32), ServerResult::ErrorCode(code) => Err(TwonlyError::Api(code)),
}
#[frb]
pub enum ServerResultVecU8 {
Ok(Vec<u8>),
ErrorCode(i32),
}
#[frb]
pub enum ServerResultI64 {
Ok(i64),
ErrorCode(i32),
}
impl ServerResult<()> {
pub fn into_bridge(self) -> ServerResultEmpty {
match self {
ServerResult::Ok(()) => ServerResultEmpty::Ok,
ServerResult::ErrorCode(c) => ServerResultEmpty::ErrorCode(c),
}
} }
} }
impl ServerResult<Vec<u8>> { fn empty_api_response(bytes: Vec<u8>) -> Result<()> {
pub fn into_bridge(self) -> ServerResultVecU8 { api_result(decode_ok_value(bytes, |value| match value {
match self { ResponseOk::None(_) => Some(()),
ServerResult::Ok(v) => ServerResultVecU8::Ok(v), _ => None,
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c), })?)
}
}
} }
impl ServerResult<i64> { fn encoded_api_response<T: Message>(
pub fn into_bridge(self) -> ServerResultI64 { bytes: Vec<u8>,
match self { extract: impl FnOnce(ResponseOk) -> Option<T>,
ServerResult::Ok(v) => ServerResultI64::Ok(v), ) -> Result<Vec<u8>> {
ServerResult::ErrorCode(c) => ServerResultI64::ErrorCode(c), api_result(decode_ok_value(bytes, extract)?).map(|value| value.encode_to_vec())
}
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -133,7 +112,7 @@ pub struct PreparedOutgoingMessage {
impl RustApi { impl RustApi {
pub async fn request_contact_by_username(username: String) -> Result<()> { pub async fn request_contact_by_username(username: String) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
crate::services::contacts::ContactService::new(ctx) ContactService::new(ctx)
.request_by_username(username, false) .request_by_username(username, false)
.await .await
} }
@ -206,122 +185,156 @@ impl RustApi {
proof_of_work: i64, proof_of_work: i64,
lang_code: String, lang_code: String,
is_ios: bool, is_ios: bool,
) -> Result<ServerResultI64> { ) -> Result<i64> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::register(ctx, username, proof_of_work, lang_code, is_ios) Server::register(ctx, username, proof_of_work, lang_code, is_ios)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn get_user_by_id(user_id: i64) -> Result<ServerResultVecU8> { pub async fn get_user_by_id(user_id: i64) -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::get_user_by_id(ctx, user_id) Server::get_user_by_id(ctx, user_id)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
.map(|value| value.encode_to_vec())
} }
pub async fn check_for_deleted_usernames() -> Result<()> { pub async fn check_for_deleted_usernames() -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::check_for_deleted_usernames(ctx).await Server::check_for_deleted_usernames(ctx).await
} }
pub async fn get_user_id_from_username(username: String) -> Result<ServerResultI64> { pub async fn get_user_id_from_username(username: String) -> Result<i64> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::get_user_id_from_username(ctx, username) Server::get_user_id_from_username(ctx, username)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn get_user_data(username: String) -> Result<ServerResultVecU8> { pub async fn get_user_data(username: String) -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::get_user_by_username(ctx, username) Server::get_user_by_username(ctx, username)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
.map(|value| value.encode_to_vec())
} }
pub async fn get_proof_of_work() -> Result<ServerResultVecU8> { pub async fn get_proof_of_work() -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::get_proof_of_work(ctx).await.map(|r| match r { Server::get_proof_of_work(ctx)
ServerResult::Ok(data) => ServerResultVecU8::Ok(data.encode_to_vec()),
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
})
}
pub async fn download_done(token: Vec<u8>) -> Result<ServerResultEmpty> {
let ctx = Context::get_static()?;
Server::download_done(ctx, token)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
.map(|value| value.encode_to_vec())
} }
pub async fn set_login_token(token: Vec<u8>) -> Result<ServerResultEmpty> { pub async fn download_done(token: Vec<u8>) -> Result<()> {
let ctx = Context::get_static()?;
Server::download_done(ctx, token).await.and_then(api_result)
}
pub async fn download_media(media_id: String) -> Result<()> {
let ctx = Context::get_static()?;
crate::services::mediafiles::MediaFileService::new(ctx)
.download_when_available(&media_id)
.await
}
pub async fn download_pending_media() -> Result<()> {
let ctx = Context::get_static()?;
crate::services::mediafiles::MediaFileService::new(ctx)
.download_pending()
.await
}
pub async fn request_media_reupload(media_id: String) -> Result<()> {
let ctx = Context::get_static()?;
crate::services::mediafiles::MediaFileService::new(ctx)
.request_reupload(&media_id)
.await
}
pub async fn set_login_token(token: Vec<u8>) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::set_login_token(ctx, token) Server::set_login_token(ctx, token)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn request_memories_upload( pub async fn request_memories_upload(
size: i64, size: i64,
original_date: i64, original_date: i64,
media_id: String, media_id: String,
) -> Result<ServerResultVecU8> { ) -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::request_memories_upload(ctx, size, original_date, media_id) Server::request_memories_upload(ctx, size, original_date, media_id)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
.map(|value| value.encode_to_vec())
} }
pub async fn get_memories_usage() -> Result<ServerResultVecU8> { pub async fn get_memories_usage() -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::get_memories_usage(ctx) Server::get_memories_usage(ctx)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
.map(|value| value.encode_to_vec())
} }
pub async fn get_memories_url(media_id: String, thumbnail: bool) -> Result<ServerResultVecU8> { pub async fn get_memories_url(media_id: String, thumbnail: bool) -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::get_memories_url(ctx, media_id, thumbnail) Server::get_memories_url(ctx, media_id, thumbnail)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
.map(|value| value.encode_to_vec())
} }
pub async fn confirm_memories_upload(media_id: String) -> Result<ServerResultEmpty> { pub async fn confirm_memories_upload(media_id: String) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::confirm_memories_upload(ctx, media_id) Server::confirm_memories_upload(ctx, media_id)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn delete_memory(media_id: String) -> Result<ServerResultEmpty> { pub async fn delete_memory(media_id: String) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::delete_memory(ctx, media_id) Server::delete_memory(ctx, media_id)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn disable_memories_backup() -> Result<ServerResultEmpty> { pub async fn disable_memories_backup() -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::disable_memories_backup(ctx) Server::disable_memories_backup(ctx)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn get_plan_balance() -> Result<Vec<u8>> { pub async fn get_plan_balance() -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::get_plan_balance(ctx).await encoded_api_response(Server::get_plan_balance(ctx).await?, |value| match value {
ResponseOk::Planballance(balance) => Some(balance),
_ => None,
})
} }
pub async fn load_plan_balance(use_cache: bool) -> Result<Vec<u8>> { pub async fn load_plan_balance() -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::load_plan_balance(ctx, use_cache).await encoded_api_response(Server::load_plan_balance(ctx).await?, |value| match value {
ResponseOk::Planballance(balance) => Some(balance),
_ => None,
})
} }
pub async fn remove_additional_user(user_id: i64) -> Result<ServerResultEmpty> { pub async fn remove_additional_user(user_id: i64) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::remove_additional_user(ctx, user_id) Server::remove_additional_user(ctx, user_id)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn add_additional_user(user_id: i64) -> Result<ServerResultEmpty> { pub async fn add_additional_user(user_id: i64) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::add_additional_user(ctx, user_id) Server::add_additional_user(ctx, user_id)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn register_passwordless_recovery( pub async fn register_passwordless_recovery(
encrypted_server_key: Vec<u8>, encrypted_server_key: Vec<u8>,
pin_unlock_token: Option<Vec<u8>>, pin_unlock_token: Option<Vec<u8>>,
) -> Result<ServerResultEmpty> { ) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::register_passwordless_recovery(ctx, encrypted_server_key, pin_unlock_token) Server::register_passwordless_recovery(ctx, encrypted_server_key, pin_unlock_token)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
}
pub async fn perform_passwordless_recovery_heartbeat() -> Result<()> {
let ctx = Context::get_static()?;
crate::api::messages::incoming::client2client::recovery::perform_heartbeat(ctx).await
} }
pub async fn get_server_key_for_passwordless_recovery( pub async fn get_server_key_for_passwordless_recovery(
user_id: i64, user_id: i64,
@ -329,7 +342,7 @@ impl RustApi {
pin_unlock_token: Option<Vec<u8>>, pin_unlock_token: Option<Vec<u8>>,
pin_protection_key: Option<Vec<u8>>, pin_protection_key: Option<Vec<u8>>,
email: Option<String>, email: Option<String>,
) -> Result<ServerResultVecU8> { ) -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::get_server_key_for_passwordless_recovery( Server::get_server_key_for_passwordless_recovery(
ctx, ctx,
@ -340,23 +353,23 @@ impl RustApi {
email, email,
) )
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn submit_recovery_share( pub async fn submit_recovery_share(
notification_id: String, notification_id: String,
encrypted_message: Vec<u8>, encrypted_message: Vec<u8>,
) -> Result<ServerResultEmpty> { ) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::submit_recovery_share(ctx, notification_id, encrypted_message) Server::submit_recovery_share(ctx, notification_id, encrypted_message)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn register_passwordless_notification( pub async fn register_passwordless_notification(
notification_id: String, notification_id: String,
download_auth_token: Vec<u8>, download_auth_token: Vec<u8>,
lang_code: String, lang_code: String,
google_fcm: Option<String>, google_fcm: Option<String>,
) -> Result<ServerResultEmpty> { ) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::register_passwordless_notification( Server::register_passwordless_notification(
ctx, ctx,
@ -366,13 +379,13 @@ impl RustApi {
google_fcm, google_fcm,
) )
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn check_for_passwordless_notification( pub async fn check_for_passwordless_notification(
notification_id: String, notification_id: String,
download_auth_token: Vec<u8>, download_auth_token: Vec<u8>,
already_received_message_ids: Vec<i64>, already_received_message_ids: Vec<i64>,
) -> Result<ServerResultVecU8> { ) -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::check_for_passwordless_notification( Server::check_for_passwordless_notification(
ctx, ctx,
@ -381,49 +394,46 @@ impl RustApi {
already_received_message_ids, already_received_message_ids,
) )
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
.map(|value| value.encode_to_vec())
} }
pub async fn report_user(user_id: i64, reason: String) -> Result<ServerResultEmpty> { pub async fn report_user(user_id: i64, reason: String) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::report_user(ctx, user_id, reason) Server::report_user(ctx, user_id, reason)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn delete_account() -> Result<ServerResultEmpty> { pub async fn delete_account() -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::delete_account(ctx).await.map(|r| r.into_bridge()) Server::delete_account(ctx).await.and_then(api_result)
} }
pub async fn update_fcm_token(token: String) -> Result<ServerResultEmpty> { pub async fn update_fcm_token(token: String) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::update_fcm_token(ctx, token) Server::update_fcm_token(ctx, token)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn ipa_purchase( pub async fn ipa_purchase(
product_id: String, product_id: String,
source: String, source: String,
verification_data: String, verification_data: String,
) -> Result<Vec<u8>> { ) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::ipa_purchase(ctx, product_id, source, verification_data).await empty_api_response(Server::ipa_purchase(ctx, product_id, source, verification_data).await?)
} }
pub async fn change_username(username: String) -> Result<ServerResultEmpty> { pub async fn change_username(username: String) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::change_username(ctx, username) Server::change_username(ctx, username)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn force_ipa_check() -> Result<ServerResultEmpty> { pub async fn force_ipa_check() -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::force_ipa_check(ctx).await.map(|r| r.into_bridge()) Server::force_ipa_check(ctx).await.and_then(api_result)
} }
pub async fn update_signed_pre_key( pub async fn update_signed_pre_key(id: i64, key: Vec<u8>, signature: Vec<u8>) -> Result<()> {
id: i64,
key: Vec<u8>,
signature: Vec<u8>,
) -> Result<Vec<u8>> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::update_signed_pre_key(ctx, id, key, signature).await empty_api_response(Server::update_signed_pre_key(ctx, id, key, signature).await?)
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn upload_pqc_pre_keys( pub async fn upload_pqc_pre_keys(
@ -434,7 +444,7 @@ impl RustApi {
kyber_signed_prekey: Vec<u8>, kyber_signed_prekey: Vec<u8>,
kyber_signed_prekey_signature: Vec<u8>, kyber_signed_prekey_signature: Vec<u8>,
prekeys: Vec<PqcPreKeyInput>, prekeys: Vec<PqcPreKeyInput>,
) -> Result<Vec<u8>> { ) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::upload_pqc_pre_keys( Server::upload_pqc_pre_keys(
ctx, ctx,
@ -447,16 +457,17 @@ impl RustApi {
prekeys, prekeys,
) )
.await .await
.and_then(empty_api_response)
} }
pub async fn send_text_message( pub async fn send_text_message(
user_id: i64, user_id: i64,
body: Vec<u8>, body: Vec<u8>,
push_data: Option<Vec<u8>>, push_data: Option<Vec<u8>>,
) -> Result<ServerResultEmpty> { ) -> Result<()> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
Server::send_text_message(ctx, user_id, body, push_data) Server::send_text_message(ctx, user_id, body, push_data)
.await .await
.map(|r| r.into_bridge()) .and_then(api_result)
} }
pub async fn send_encrypted_content( pub async fn send_encrypted_content(
@ -587,48 +598,3 @@ impl RustApi {
.await .await
} }
} }
impl ServerResult<crate::api::proto::server_to_client::response::UserData> {
pub fn into_bridge(self) -> ServerResultVecU8 {
match self {
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
}
}
}
impl ServerResult<crate::api::proto::server_to_client::response::PasswordlessNotificationMessages> {
pub fn into_bridge(self) -> ServerResultVecU8 {
match self {
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
}
}
}
impl ServerResult<crate::api::proto::server_to_client::response::MemoriesUploadUrls> {
pub fn into_bridge(self) -> ServerResultVecU8 {
match self {
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
}
}
}
impl ServerResult<crate::api::proto::server_to_client::response::MemoriesUsage> {
pub fn into_bridge(self) -> ServerResultVecU8 {
match self {
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
}
}
}
impl ServerResult<crate::api::proto::server_to_client::response::MemoriesUrl> {
pub fn into_bridge(self) -> ServerResultVecU8 {
match self {
ServerResult::Ok(v) => ServerResultVecU8::Ok(prost::Message::encode_to_vec(&v)),
ServerResult::ErrorCode(c) => ServerResultVecU8::ErrorCode(c),
}
}
}

View file

@ -54,15 +54,11 @@ callback_generator! {
}, },
Api api { Api api {
resync_signal_session: (i64) => (), resync_signal_session: (i64) => (),
push_key_requested: (i64) => (),
group_membership_error: (i64, String, String) => (),
media_action: (String, String, i64, String) => (), media_action: (String, String, i64, String) => (),
verification_proof: (i64, Vec<u8>) => (), verification_proof: (i64, Vec<u8>) => (),
create_push_data: (i64, Option<String>, Vec<u8>, i32) => Option<Vec<u8>>,
create_push_avatars: (i64) => (), create_push_avatars: (i64) => (),
recovery_changed: () => (),
media_received: (String, i64) => (), media_received: (String, i64) => (),
group_state_refresh: (String, bool) => () user_config_changed: (crate::user_config::UserConfig) => ()
} }
} }
} }

View file

@ -7,6 +7,7 @@
pub mod api; pub mod api;
pub mod callbacks; pub mod callbacks;
pub mod groups; pub mod groups;
pub mod user_config;
pub mod wrapper; pub mod wrapper;
use std::sync::Arc; use std::sync::Arc;
@ -20,13 +21,12 @@ use crate::error::TwonlyError;
use crate::keys::KeyManager; use crate::keys::KeyManager;
use crate::secure_storage::SecureStorage; use crate::secure_storage::SecureStorage;
use crate::signal::engine::RustSignalEngine; use crate::signal::engine::RustSignalEngine;
use crate::user_discovery::stores::{NativeUserDiscoveryStore, NativeUserDiscoveryUtils};
use crate::user_discovery::UserDiscovery; use crate::user_discovery::UserDiscovery;
use crate::utils::Shared; use crate::utils::Shared;
use flutter_rust_bridge::frb; use flutter_rust_bridge::frb;
pub use crate::user_discovery::traits::AnnouncedUser; pub use crate::user_discovery::AnnouncedUser;
pub use crate::user_discovery::traits::OtherPromotion; pub use crate::user_discovery::OtherPromotion;
use tokio::sync::{Mutex, OnceCell, RwLock}; use tokio::sync::{Mutex, OnceCell, RwLock};
pub struct InitConfig { pub struct InitConfig {
@ -54,8 +54,7 @@ pub struct _AnnouncedUser {
pub(crate) struct TwonlyFlutter { pub(crate) struct TwonlyFlutter {
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) config: InitConfig, pub(crate) config: InitConfig,
pub(crate) user_discovery: pub(crate) user_discovery: Shared<UserDiscovery>,
Shared<UserDiscovery<NativeUserDiscoveryStore, NativeUserDiscoveryUtils>>,
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) rust_db: Arc<RwLock<Arc<Database>>>, pub(crate) rust_db: Arc<RwLock<Arc<Database>>>,
pub(crate) app_db: Arc<RwLock<Arc<AppDatabase>>>, pub(crate) app_db: Arc<RwLock<Arc<AppDatabase>>>,

View file

@ -0,0 +1,70 @@
use crate::context::Context;
use crate::error::Result;
use crate::user_config::UserConfig;
pub struct UserConfigApi {}
impl UserConfigApi {
pub async fn load() -> Result<Option<UserConfig>> {
let ctx = Context::get_static()?;
UserConfig::load_from(ctx)
}
pub async fn create(
user_id: i64,
username: String,
display_name: String,
current_setup_page: Option<String>,
app_version: i64,
) -> Result<UserConfig> {
serde_json::from_value(serde_json::json!({
"userId": user_id,
"username": username,
"displayName": display_name,
"subscriptionPlan": "Free",
"currentSetupPage": current_setup_page,
"appVersion": app_version,
}))
.map_err(Into::into)
}
#[flutter_rust_bridge::frb(sync)]
pub fn clone(config: UserConfig) -> UserConfig {
config
}
pub async fn save(config: UserConfig) -> Result<UserConfig> {
let ctx = Context::get_static()?;
let normalized = UserConfig::save_json(ctx, &serde_json::to_string(&config)?)?;
let config: UserConfig = serde_json::from_str(&normalized)?;
let mut key_manager = ctx.get_key_manager().await?;
if key_manager.user_id != Some(config.user_id) {
key_manager.user_id = Some(config.user_id);
key_manager.store_to_keychain(ctx.get_secure_storage())?;
}
drop(key_manager);
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
(callbacks.api.user_config_changed)(config.clone()).await;
}
Ok(config)
}
pub async fn update(base: UserConfig, config: UserConfig) -> Result<UserConfig> {
let ctx = Context::get_static()?;
let normalized = UserConfig::update_json(
ctx,
&serde_json::to_string(&base)?,
&serde_json::to_string(&config)?,
)?;
let config: UserConfig = serde_json::from_str(&normalized)?;
if let Ok(callbacks) = crate::bridge::callbacks::get_callbacks() {
(callbacks.api.user_config_changed)(config.clone()).await;
}
Ok(config)
}
pub async fn import_json(json: String) -> Result<UserConfig> {
let config: UserConfig = serde_json::from_str(&json)?;
Self::save(config).await
}
}

Some files were not shown because too many files have changed in this diff Show more